In Confluence 9.3.1 REST requests don't work with our plugin installed, but in Confluence < 9.3.1 e.g. in 9.2.1 version all worked well

Hello, our plugin track REST system events and intercept related REST requests. Specifically in Confluence 9.3.1 with our plugin installed there is a problem that all these Confluence REST requests that we intercept do not work. And for these requests we have
400 bad request status in response and also this message in response e.g. for case when we backup site:
"Trailing token (of type START_OBJECT) found after value (bound as com.atlassian.confluence.api.model.backuprestore.SiteBackupSettings): not allowed as per DeserializationFeature.FAIL_ON_TRAILING_TOKENS

at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 69]"

But in Confluence < 9.3.1 e.g. in 9.2.1 version all worked well.

Could you please explain how to fix this problem? It is very important and urgent for us.

Did you solve the problem already? We have a similar issue and I’m pretty certain that this is caused by improper wrapping of the request being intercepted by the filter. I tried quite a few variations different to the wrapper we are currently using but with no success so far.

Yes, I solved the problem using my own CompatibilityRequestWrapper

    HttpServletRequestWrapper wrappedRequest;
    try {
      wrappedRequest = new RestReadingServletRequest((HttpServletRequest) request);
      if (isConfluenceApplication) {
        wrappedRequest = new CompatibilityRequestWrapper(wrappedRequest);
      }
    } catch (Exception e) {
      log.error(e);
      chain.doFilter(request, response);
      return;
    }

    chain.doFilter(wrappedRequest, response);

RestReadingServletRequest have logic to create multi-readable request that reads request body to temp var and overrides getReader and getInputStream to return this temp stored data


import java.io.BufferedReader;
import java.io.IOException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * This class in needed to fix error in Confluence 9.3.1: "Trailing token (of type START_OBJECT)
 * found after value: not allowed as per `DeserializationFeature.FAIL_ON_TRAILING_TOKENS`" error.
 */
public class CompatibilityRequestWrapper extends HttpServletRequestWrapper {

  private static final FormattingLogger log =
      FormattingLogger.getLogger(CompatibilityRequestWrapper.class);

  private CompatibilityInputStream compatibilityInputStream;
  private CompatibilityBufferedReader compatibilityBufferedReader;

  public CompatibilityRequestWrapper(HttpServletRequest httpServletRequest) {
    super(httpServletRequest);
  }

  public ServletInputStream getInputStream() throws IOException {
    if (this.compatibilityInputStream == null) {
      this.compatibilityInputStream = new CompatibilityInputStream(super.getInputStream());
    }
    return this.compatibilityInputStream;
  }

  public BufferedReader getReader() throws IOException {
    if (this.compatibilityBufferedReader == null) {
      this.compatibilityBufferedReader = new CompatibilityBufferedReader(super.getReader());
    }

    return this.compatibilityBufferedReader;
  }

  private class CompatibilityInputStream extends ServletInputStream {
    private final ServletInputStream delegate;

    private CompatibilityInputStream(ServletInputStream delegate) {
      this.delegate = delegate;
    }

    @Override
    public int read() throws IOException {
      return this.delegate.read();
    }

    @Override
    public int read(byte[] b) throws IOException {
      return this.delegate.read(b);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
      return this.delegate.read(b, off, len);
    }

    @Override
    public int readLine(byte[] b, int off, int len) throws IOException {
      return this.delegate.readLine(b, off, len);
    }

    @Override
    public long skip(long n) throws IOException {
      return this.delegate.skip(n);
    }

    @Override
    public int available() throws IOException {
      return this.delegate.available();
    }

    @Override
    public void close() throws IOException {
      this.delegate.close();
    }

    @Override
    public void mark(int readlimit) {
      this.delegate.mark(readlimit);
    }

    @Override
    public void reset() throws IOException {
      this.delegate.reset();
    }

    @Override
    public boolean markSupported() {
      return this.delegate.markSupported();
    }
  }

  private class CompatibilityBufferedReader extends BufferedReader {
    private final BufferedReader delegate;

    private CompatibilityBufferedReader(BufferedReader delegate) {
      super(delegate);
      this.delegate = delegate;
    }

    @Override
    public int read() throws IOException {
      return this.delegate.read();
    }

    @Override
    public String readLine() throws IOException {
      return this.delegate.readLine();
    }

    @Override
    public int read(char[] cbuf) throws IOException {
      return this.delegate.read(cbuf);
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
      return this.delegate.read(cbuf, off, len);
    }

    @Override
    public long skip(long n) throws IOException {
      return this.delegate.skip(n);
    }

    @Override
    public boolean ready() throws IOException {
      return this.delegate.ready();
    }

    @Override
    public boolean markSupported() {
      return this.delegate.markSupported();
    }

    @Override
    public void mark(int readAheadLimit) throws IOException {
      this.delegate.mark(readAheadLimit);
    }

    @Override
    public void reset() throws IOException {
      this.delegate.reset();
    }

    @Override
    public void close() throws IOException {
      this.delegate.close();
    }
  }
}

It’s great to hear that you fixed it and I’m just trying to adopt the same changes. I have question though: I’m getting “Class ‘CompatibilityInputStream’ must either be declared abstract or implement abstract methods ‘isFinished(), isReady() and setReadListener()’ in ‘ServletInputStream’”. I’m wondering why you don’t need to have these. I can see that ServletInputStream comes from javax.servlet-api:3.1.0. What’s your version?

Also, if feasible, it would be great to see your full RestReadingServletRequest class. Just in case combining your CompatibilityRequestWrapper with our original RequestWrapper won’t work.

Lastly: I highly appreciate replying and providing all these details :tada:

It’s great to hear that you fixed it and I’m just trying to adopt the same changes. I have question though: I’m getting “Class ‘CompatibilityInputStream’ must either be declared abstract or implement abstract methods ‘isFinished(), isReady() and setReadListener()’ in ‘ServletInputStream’”. I’m wondering why you don’t need to have these. I can see that ServletInputStream comes from javax.servlet-api:3.1.0. What’s your version?

I think you can try to add implementation of missing methods if you use sevlet api with higher versions
I used this dependency i.e. servlet api version is 2.3

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.3</version>
            <scope>provided</scope>
        </dependency>

Also, if feasible, it would be great to see your full RestReadingServletRequest class. Just in case combining your CompatibilityRequestWrapper with our original RequestWrapper won’t work.


import org.apache.log4j.Logger;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RestReadingServletRequest extends HttpServletRequestWrapper {
  private static final Logger log =
      Logger.getLogger(RestReadingServletRequest.class);
  private final byte[] body;

  public RestReadingServletRequest(HttpServletRequest request) throws IOException {
    super(request);
    BufferedInputStream bis = null;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
      InputStream inputStream = request.getInputStream();
      if (inputStream != null) {
        bis = new BufferedInputStream(inputStream);
        byte[] byteBuffer = new byte[1024];
        int bytesRead = -1;
        while ((bytesRead = bis.read(byteBuffer)) > 0) {
          baos.write(byteBuffer, 0, bytesRead);
        }
      }
    } catch (IOException ex) {
      log.error(String.format("Error reading request input stream: %s", ex.getMessage()));
      throw ex;
    } finally {
      if (bis != null) {
        try {
          bis.close();
          baos.close();
        } catch (IOException ex) {
          log.error(String.format("Error closing request input stream: %s", ex.getMessage()));
          throw ex;
        }
      }
    }
    body = baos.toByteArray();
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
    ServletInputStream servletInputStream = new ServletInputStream() {
      public int read() throws IOException {
        return byteArrayInputStream.read();
      }
    };
    return servletInputStream;
  }

  @Override
  public BufferedReader getReader() throws IOException {
    return new BufferedReader(new InputStreamReader(this.getInputStream()));
  }

  public String getBody() {
    try {
      return new String(body, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      return new String(body);
    }
  }
}


1 Like

Awesome. Let me work with that a bit and see if I can fix my problem. Again, I highly appreciate your feedback. I’m sure this thread will be(come) interesting for other P2 developers as well.
Especially given the fact that this problem only started with recent versions of Jira and Confluence.

this problem only started with recent versions of Jira

In which Jira version this problem also exist? I reproduced this problem only in Confluence 9.3.1. I didn’t reproduce this problem on Jira 10.4. But I haven’t tested yet is this problem reproduced on Jira 10.5. Problem exist on Jira 10.5 or also in earlier Jira versions? Could you please provide Jira version on which you reproduced the problem? Could you please also provide requests on which this problem reproduced in Jira?

In Jira Software 10.4.0 everything worked as it did for 2+ years.

It changed with 10.4.1.
We intercept a request for the /rest/tsv/1.0/authenticate endpoint that belongs to the new login form (which also exists in Confluence). But I only need to intercept it in Jira and I’m sure the same would happen in Confluence if I had to.

If you enter your username and password in the new login form, the data is POSTed to that endpoint.

And to be more precise: We also need to read (the username) from the request payload.

There’s still a bit of testing required (with older Jira version etc.) but at least it works already in my Jira Software 10.5.0. This ends a few-days journey of trying multiple things and I’m more than happy. :star:

OK, thank you for your replies, will we check does problem exist also on Jira on our side for requests that are relevant for us.

Thank you!

Hi everyone,
If anyone has faced this issue or has a workaround, please share your experience. Are there any official updates from Atlassian on how to handle request payloads in filters after Jira 10.5?

Any guidance or suggestions would be very helpful

After upgrading from Jira 10.3.6 to 10.5.1, our plugin that intercepts REST payloads for /rest/api/2/issue/{id} began failing with HTTP 400 Bad Request. We use a servlet filter to restrict label updates through the REST API.

This worked fine in earlier Jira versions, but now we’re getting this error:

Trailing token (of type START_OBJECT) found after value (bound as `com.atlassian.jira.rest.v2.issue.IssueUpdateBean`): not allowed as per `DeserializationFeature.FAIL_ON_TRAILING_TOKENS`\n at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 9, column: 2] [a234b4c1-74bc-4b19-851e-d9e15664d1a2]"

Request Wrapper class

public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // Read and cache the body
        InputStream inputStream = request.getInputStream();
        this.body = new BufferedReader(new InputStreamReader(inputStream))
                .lines()
                .collect(Collectors.joining("\n"));
    }

    @Override
    public ServletInputStream getInputStream() {
        // Return a new input stream with the cached body
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(javax.servlet.ReadListener readListener) {
                // Not implemented
            }
        };
    }

    public String getBody() {
        return this.body;
    }
}
@Order(1)
public class IssueServletFilter implements Filter {
    private final Logger logger = LoggerFactory.getLogger(IssueServletFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Wrap request
        RequestWrapper wrappedRequest = new RequestWrapper(httpRequest);

        String payload = wrappedRequest.getBody();

        chain.doFilter(wrappedRequest, httpResponse);
    }
}

Hello, could you please try solution described in In Confluence 9.3.1 REST requests don't work with our plugin installed, but in Confluence < 9.3.1 e.g. in 9.2.1 version all worked well - #3 by AlexeyAleinikov? Will it help to solve the problem for your case?

1 Like

Yes, I tried this, and it worked for me as well. I’d also appreciate it if you could share what is causing this issue in the recent Jira/Confluence versions.

I think it problem occurred because in new Jira/Confluence versions some internal logic was changed, maybe some dependency version or some internal config was changed and that’s why old logic of capturing request body didn’t work now. I can’t say what exactly causing this issue, but I also faced it and tried different approaches to fix it and I found a way to fix this problem accidentally during testing different approaches and implementations of capturing servlet request body content.
Looking at logs
“Trailing token (of type START_OBJECT) found after value (bound as com.atlassian.jira.rest.v2.issue.IssueUpdateBean): not allowed as per DeserializationFeature.FAIL_ON_TRAILING_TOKENS\n at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 9, column: 2] [a234b4c1-74bc-4b19-851e-d9e15664d1a2]”"
seems like trailing token was added to JSON text in request body internally by Jira/Confluence when you used your old approach or capturing request body, I suppose Jira/Confluence for some reason added this token for your old request wrapper because without the wrapper all should work well. Maybe previously Jira/Confluence also added this token but FAIL_ON_TRAILING_TOKENS feature was disabled and that’s why you didn’t have error or in new Jira/Confluence versions trailing token was added to request body if you put request wrapper instead of non-wrapped request but in previous of Jira/Confluence it was not added.

1 Like