Hello everybody,
I developped an Atlassian connect addon using spring-boot. In the html veiw I make a POST request using ajax, when I uppload the connect addon into JIRA cloud instance and making the request, Java display this error.
java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055) ~[?:1.8.0_91] at org.springframework.http.HttpHeaders.add(HttpHeaders.java:1031) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE] at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor$JwtSignedHttpRequestWrapper.setJwtHeaders(JwtSigningClientHttpRequestInterceptor.java:171) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?] at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor$JwtSignedHttpRequestWrapper.<init>(JwtSigningClientHttpRequestInterceptor.java:161) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?] at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor.wrapRequest(JwtSigningClientHttpRequestInterceptor.java:115) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?] at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor.lambda$intercept$0(JwtSigningClientHttpRequestInterceptor.java:43) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?] at java.util.Optional.map(Optional.java:215) ~[?:1.8.0_91]
There is a ticket about this https://ecosystem.atlassian.net/browse/ACSPRING-19
In my pom.xml I use
<dependency> <groupId>com.atlassian.connect</groupId> <artifactId>atlassian-connect-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
When I go to the exception the private class is not like in the fix https://ecosystem.atlassian.net/browse/ACSPRING-19
private class JwtSignedHttpRequestWrapper extends HttpRequestWrapper { private final String jwt; private final URI uri; public JwtSignedHttpRequestWrapper(HttpRequest request, String jwt, URI uri) { super(request); this.jwt = jwt; this.uri = uri; setJwtHeaders(); // this still here } @Override public URI getURI() { return uri; } private void setJwtHeaders() { HttpHeaders headers = super.getHeaders(); headers.add(HttpHeaders.AUTHORIZATION, String.format("JWT %s", jwt)); headers.add(HttpHeaders.USER_AGENT, String.format("%s/%s", USER_AGENT_PRODUCT, atlassianConnectClientVersion)); } }
Would you have any suggestions how I can fix this issue ?
Best regards
Community moderators have prevented the ability to post new answers.
With your last comment, it's finally clear to me what is going on.
Problem
For any HTTP requests with a body in org.springframework.web.client.RestTemplate
(POST, PUT or exchange()
), the inner HttpEntityRequestCallback
class copies any headers provided for the HttpEntity
to the request. This is a good idea because the HttpHeaders
in the HttpEntity
are made immutable when the HttpEntity
is constructed. However, copying the values of the HttpHeaders
only overcomes the fact that the map of headers is immutable. But it doesn't consider that each list of header values is also immutable.
This breaks because you set the Authorization
header, and then JwtSigningClientHttpRequestInterceptor
tries to add a value for the same header.
Solution
Instead of auto-wiring a RestTemplate
object and thereby using the JwtSigningRestTemplate
provided by atlassian-connect-spring-boot, you should create a new RestTemplate
using the default constructor.
Follow-Up
To avoid this somewhat cryptic error message, I will update JwtSigningRestTemplate
to overwrite any existing Authorization
header value (since that header is only allowed to have one value).
Other headers (especially custom ones) could potentially accept multiple values though, so I'll raise this as a bug in the Spring project as well (even though it could be considered misuse of the framework API).
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Brahim ESSALIH, I was able to reproduce this problem using this ClientHttpRequestInterceptor
. Is there any chance you have added an interceptor that returns a read-only HttpHeaders
?
@Component public class BadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { return execution.execute(new HttpRequestWrapper(request) { @Override public HttpHeaders getHeaders() { return HttpHeaders.readOnlyHttpHeaders(super.getHeaders()); } }, body); } }
(Looking at the stack trace from your comment on another question, I first thought your use of RestTemplate#exchange(...)
may have triggered this problem due to the call to HttpHeaders.readOnlyHttpHeaders(HttpHeaders)
in the constructor of HttpEntity
, but I wasn't able to confirm that. It seems like Spring properly copies headers, and never runs into problems with that instance being read-only.)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks @Einar Pehrson, So you think that restemplate.exchange... was the cause of that error. Would you have any alernatif of the method exchange using Httpheader ?
When I don't use basic authentication headers it works without problem
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
No, @Brahim ESSALIH, what I meant was that I had a guess that exchange()
was the problem, but it turned out to be wrong. I wrote a passing test using exchange()
.
Rather, I think the problem is that somewhere you have a piece of code that calls HttpHeaders.readOnlyHttpHeaders()
. You could try setting a breakpoint inside that method.
When I don't use basic authentication headers it works without problem
I'm not sure what you mean by this. I wonder if you're trying to use both Basic authentication and JWT authentication with a single RestTemplate
.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
I mean by this: @Einar Pehrson
When I don't use basic authentication headers it works without problem
that when I disable basic authentication and then making request to the web service without adding header using restemplate.postforEntity or putForEntity... all works good.
I have this problem only when the Webhook triggre my service that make a request using Http basic authentication header
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hm, I don't think I have the full context of your problem then, @Brahim ESSALIH.
So your add-on receives a webhook from JIRA. And as part of handling that webhook, you make a request to another service using Basic authentication? If so, what code do you use to make the request? How do you create and configure that RestTemplate
(or whichever client you use)? How do you create the Authorization header for that request?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Einar Pehrson this is my context:
In my descriptor.json I define this webhook like this snippet:
"webhooks": [ { "name": "Issue created event", "event": "jira:issue_created", "filter": "project = DEVTEST", "url": "/webhook/issueEvent", "excludeBody": false }, ...
This is how I create Authirization header:
import org.springframework.http.HttpHeaders; import org.apache.commons.codec.binary.Base64; // Authorization Header public HttpHeaders getHeader() { HttpHeaders headers = new HttpHeaders(); String plainCreds = userJira + ":" + userPasswordJira; byte[] plainCredsBytes = plainCreds.getBytes(); byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes); String base64Creds = new String(base64CredsBytes); headers.add("Authorization", "Basic " + base64Creds); return headers; }
If the service make a request HTTP using resttemplate.exchange, I get errors like in this post https://answers.atlassian.com/questions/44341020
This is my service:
... import org.springframework.http.HttpEntity; @Autowired RestTemplate restTemplate; // Because I use this dependency: "atlassian-connect-spring-boot-starter" @RequestMapping(value = "/webhook/issueEvent", method = RequestMethod.POST) public String issueEvent(@RequestBody WebhookBodyWrapper webhookBody) throws JsonProcessingException { ... HttpEntity<IssueUserWrapper> httpEntity = new HttpEntity<IssueUserWrapper>(assigneeWrapper, getHeader()); // THIS THROWs ERROR BELOW restTemplate.exchange(url, HttpMethod.PUT, httpEntity, Object.class); return statusCode; }
I hope I was clear enough
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Brahim ESSALIH, do you still see this error or were you able work around it? Can you provide any more information or perhaps steps to reproduce?
The stack trace can hardly be related to an AJAX POST request, since the responsibility of JwtSigningClientHttpRequestInterceptor
is to add an Authorization
header for JWT authentication to outbound requests made from the server-side of your Spring Boot application.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi, @Brahim ESSALIH , the issue you mention is fixed in 1.0.0 so that specific bug is unlikely to be the problem.
Is there any chance you could share some of your code? It's hard to work out what's going wrong with just the stack trace and the connect-spring-boot code.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Community moderators have prevented the ability to post new answers.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.