I have a JAX-RS REST resource which should be able to respond in different charsets as indicated by the client's preference through the Accept-Charset header.
However, as JAX-RS seems to ignore the Accept-Charset header by default, I wrote two methods explicitly stating the two different charsets I want to support:
#GET
#Path("test")
#Produces("text/plain; charset=UTF-8")
public String test_utf8() {
return "Hello World";
}
#GET
#Path("test")
#Produces("text/plain; charset=cp1047")
public String test_cp1047() {
return "Hello World";
}
However, when now calling the method using curl:
curl -v -H "Accept-Charset: cp1047;q=1.0, *;q=0" "http://localhost:8080/rest/test" -H "Accept: text/plain"
The server responds in UTF-8:
> GET /rest/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.50.3
> Accept-Charset: cp1047;q=1.0, *;q=0
> Accept: text/plain
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 19
< Date: Mon, 06 Jul 2020 21:29:11 GMT
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
Hello World
In addition, a log message appears in the server log:
23:29:11,356 WARN [org.jboss.resteasy.resteasy_jaxrs.i18n] (default
task-2) RESTEASY002142: Multiple resource methods match request "GET
/test". Selecting one. Matching methods: [public java.lang.String
org.example.RestResource.test_utf8(), public java.lang.String
org.example.RestResource.test_cp1047()]
How can I force the server to honor the charset requested by the client?
Solved it by creating a custom ContainerResponseFilter which parses the Accept-Charset header and uses the "best" charset as requested by the client:
#Provider
#Priority(Priorities.HEADER_DECORATOR)
#HonorAcceptCharset
public class HonorAcceptCharsetFilter implements ContainerResponseFilter {
private static final Pattern AcceptedEncodingPattern = Pattern.compile("([A-Za-z*0-9_\\-]+)(?:; *[qQ] *= *([0-9]+(?:\\.[0-9]+)?))?");
public static final String AcceptCharsetHeaderName = "Accept-Charset";
public static final String ContentTypeHeaderName = "Content-Type";
#Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
MediaType mediaType = responseContext.getMediaType();
if (mediaType == null)
return;
Charset charset = this.determineCharset(requestContext);
if (charset == null)
return;
responseContext.getHeaders().putSingle(ContentTypeHeaderName, mediaType.withCharset(charset.name()));
}
private Charset determineCharset(ContainerRequestContext requestContext) {
String acceptCharset = requestContext.getHeaderString(AcceptCharsetHeaderName);
if (acceptCharset == null)
return null;
List<Map.Entry<Charset, Double>> acceptedCharsets = Arrays.stream(acceptCharset.split(", *"))
.map(this::parseAcceptedEncoding)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (acceptCharset.length() == 0)
return null;
if (acceptCharset.length() == 1)
return acceptedCharsets.get(0).getKey();
OptionalDouble maxQuality = acceptedCharsets.stream().mapToDouble(Map.Entry::getValue).max();
List<Charset> candidates = acceptedCharsets.stream().filter(it -> it.getValue() == maxQuality.getAsDouble()).map(Map.Entry::getKey).collect(Collectors.toList());
if (candidates.size() == 1)
return candidates.get(0);
if (candidates.stream().anyMatch(it -> it.name().toLowerCase().matches("utf-?8")))
return StandardCharsets.UTF_8;
return candidates.get(0);
}
private Map.Entry<Charset, Double> parseAcceptedEncoding(String acceptedEncoding) {
Matcher matcher = AcceptedEncodingPattern.matcher(acceptedEncoding);
if (!matcher.find())
return null;
String charsetName = matcher.group(1);
if ("*".equals(charsetName))
return null;
try {
if (!Charset.isSupported(charsetName))
return null;
} catch (IllegalCharsetNameException ex) {
return null;
}
Charset charset = Charset.forName(charsetName);
String quality = matcher.group(2);
double qualityNumber = StringUtils.isAllEmpty(quality) ? 1.0 : Double.parseDouble(quality);
return new AbstractMap.SimpleEntry<>(charset, qualityNumber);
}
}
#NameBinding
#Retention(RetentionPolicy.RUNTIME)
public #interface HonorAcceptCharset {
}
Now I just need one method which I can decorate with #HonorAcceptCharset in order to enable the filter (I don't want to enable it globally as I do not want to add charsets to binary types. Maybe the filter could look into the media type and determine if it wants to activate depending on a predefined list as an alternative.
Still strikes me somewhat strange that this kind of behaviour is not enabled by default.
Related
I use Spring to call a REST API which reply with header that contains "Content-Type: multipart/mixed; boundary==_123456789".
My problem is that Spring try to parse the Content-Type and throw InvalidMediaTypeException due to '=' in boundary token.
Is there a way to disable this checking (or any workaround) ?
These are my logs :
org.springframework.http.InvalidMediaTypeException: Invalid mime type "multipart/mixed; boundary==_11204303fda4403b6a72d61500081354": Invalid token character '=' in token "=_11204303fda4403b6a72d61500081354"
at org.springframework.http.MediaType.parseMediaType(MediaType.java:620)
at org.springframework.http.HttpHeaders.getContentType(HttpHeaders.java:992)
at org.springframework.web.client.HttpMessageConverterExtractor.getContentType(HttpMessageConverterExtractor.java:136)
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:93)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1037)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1020)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:778)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:468)
RFC1341 applies to MIME types in email.
For HTTP the specification is in RFC2616 § 2.2, and that document specifies that "token separators" can be included as values only if the value is enclosed in quotes. It is possible that whatever is creating that boundary string is using the RFC1341 definition instead of the (correct) RFC2616 spec.
The header should be
multipart/mixed; boundary="=_11204303fda4403b6a72d61500081354"
BTW, the code that throws the exception is here in method checkToken(). That's in the latest version, looks like you have an earlier version of Spring.
I found a way by using ClientHttpRequestInterceptor to modify the boundary parameter and add double quote.
public class MultipartMixedBoundaryRewritingInterceptor implements ClientHttpRequestInterceptor {
private Pattern multipartMixedHeaderPattern = Pattern.compile("^multipart/mixed;\\s?boundary=(.*?)$");
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse response = execution.execute(request, body);
HttpHeaders responseHeaders = response.getHeaders();
// Do not use .getHeaders().getContentType() since it checks everything and raises an exception on the invalid boundary
String contentType = response.getHeaders().getFirst("Content-Type");
Matcher matcher = multipartMixedHeaderPattern.matcher(contentType);
if (contentType != null && matcher.matches()) {
String boundaryParameter = matcher.group(1);
try {
responseHeaders.getContentType();
} catch (IllegalArgumentException e) {
if (boundaryParameter != null && !isQuotedString(boundaryParameter)) {
Map<String, String> parameters = new HashMap<>();
parameters.put("boundary", String.format("\"%s\"", boundaryParameter));
MediaType fixedContentType = new MediaType(MediaType.MULTIPART_MIXED, parameters);
response.getHeaders().setContentType(fixedContentType);
}
}
}
return response;
}
private boolean isQuotedString(String s) {
return s.length() > 2 && ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
}
}
And configuring the restTemplate
#Configuration
public class RestTemplateConfig {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
configureRestTemplateInterceptors(restTemplate);
return restTemplate;
}
private void configureRestTemplateInterceptors(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new MultipartMixedBoundaryRewritingInterceptor());
restTemplate.setInterceptors(interceptors);
}
}
I've created a route to allow me to forward a REST call. Everything is going well, except I cannot modify the HTTP headers of the response (actually I can't even get them untouched on the response).
// My processor
private void remplacerLiensDansHeader(final Exchange exchange, final String rootUrlPivotJoram, final String rootUrlRemplacement) {
// That is OK, I get the values just fine
ArrayList<String> oldLinks = exchange.getIn().getHeader(HEADER_LINK, ArrayList.class);
// This is also OK
final List<String> newLinks = anciensLiens.stream().map(lien -> lien.replace(rootUrlPivotJoram, rootUrlRemplacement)).collect(toList());
// No error, but apparently doesn't work
exchange.getMessage().setHeader(HEADER_LINK, newLinks);
exchange.getMessage().setHeader("test", "test");
}
// Route configuration
#Override
public void configure() throws Exception {
this.from(RestRouteBuilder.endPoint(createProducerJoramConfiguration))
.setExchangePattern(InOut)
.removeHeader(Exchange.HTTP_URI)
.toD(createProducerJoramConfiguration.getUrlDestination())
.setHeader("test", () -> "test") // that doesn't work either
.process(createProducerJoramConfiguration.getProcessor()); // this is the processor with the code above
}
This is the response I get (note that the response code is 200 and I think it's weird as the original is 201)
curl -v -XPost --user "xxx:yyyy" http://localhost:10015/zzzz/webservices/xxxxx
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 10015 (#0)
* Server auth using Basic with user 'xxx'
> Post /zzzzz/webservices/eeee HTTP/1.1
> Host: localhost:10015
> Authorization: Basic pppppppppppppppppp==
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 200
< Date: Tue, 31 Aug 2021 11:17:49 GMT
< Content-Type: application/octet-stream
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Two things I've noticed:
if I add a body in the processor, then the body is present in the response,
if I remove the processor, the headers present in the "original response" are not present either.
I don't know what headers you exactly lose, but be aware that the Camel HTTP component has a default header filter (as lots of Camel components have).
If you don't specify your own HeaderFilterStrategy, the default HttpHeaderFilterStrategy is used.
This default filters the following headers:
content-length
content-type
host
cache-control
connection
date
pragma
trailer
transfer-encoding
upgrade
via
warning
Camel*
org.apache.camel*
With this filter, Camel wants to avoid that old HTTP headers are still present on outgoing requests (probably with wrong data).
The filtering of Camel headers is just to remove Camel specific stuff that is not relevant for HTTP.
Actually, the problem was the cxfrs component.
We manage to find an answer here : see : Response to REST client from Camel CXFRS route?
Here is the final solution.
Thanks to everyone that looked or answer, I hope that'll help someone else.
public class ModificationHeaderLinkProcessor implements Processor {
private static final String HEADER_LINK = "Link";
#Override
public void process(final Exchange exchange) {
List<String> newLinks= getNewLinks(exchange, oldUrl, newUrl);
ResponseBuilder builder = createHttpResponse(exchange);
builder.header(HEADER_LINK, newLinks);
exchange.getIn().setBody(builder.build());
}
private List<String> getNewLinks(final Exchange exchange, final String oldUrl, final String newUrl) {
ArrayList<String> oldLinks= exchange.getIn().getHeader(HEADER_LINK, ArrayList.class);
return oldLinks.stream().map(link-> link.replace(oldUrl, newUrl)).collect(toList());
}
private ResponseBuilder createHttpResponse(final Exchange exchange) {
ResponseBuilder builder = Response.status(getHttpStatusCode(exchange))
.entity(exchange.getIn().getBody());
clearUselessHeader(exchange);
exchange.getIn().getHeaders().forEach(builder::header);
return builder;
}
private void clearUselessHeader(final Exchange exchange) {
exchange.getIn().removeHeader(HEADER_LINK);
exchange.getIn().removeHeaders("Camel*");
}
private Integer getHttpStatusCode(final Exchange exchange) {
return exchange.getIn().getHeader(exchange.HTTP_RESPONSE_CODE, Integer.class);
}
private final String getPropertiesValue(CamelContext camelContext, String key) {
return camelContext.getPropertiesComponent().resolveProperty(key).orElseThrow();
}
}
I have a rest API implemented in Java (MSF4J codegen from swagger) and a swagger 2 definition that describes it.
A swagger UI is hosted on a web server. The API is deployed on a VM somewhere on the internet.
My Problem is that the "try it out" function of the swagger UI doesn't work. I always get a "401 Unauthorized". When I take the curl command from the UI and paste it into my terminal it works.
Last week I didn't have HTTPS or Basic Authentication - just HTTP - and it worked fine. Now I don't know why it doesn't work.
Since I changed the swagger definition to https the UI makes an OPTIONS request. I implemented that, but I get 401 responses.
The certificate comes from Lets Encrypt and is used by an apache web server. The apache is a proxy to the rest api on the same machine.
Here is my authentication interceptor:
public class BasicAuthSecurityInterceptor extends AbstractBasicAuthSecurityInterceptor {
#Override
protected boolean authenticate(String username, String password) {
if (checkCredentials(username, password))
return true;
return false;
}
private boolean checkCredentials(String username, String password) {
if (username.equals("testuser"))
return BCrypt.checkpw(password, "$2a$10$iXRsLgkJg3ZZGy4utrdNyunHcamiL2RmrKHKyJAoV4kHVGhFv.d6G");
return false;
}
}
Here is a part of the api:
public abstract class DeviceApiService {
private static final Logger LOGGER = LogManager.getLogger();
public abstract Response deviceGet() throws NotFoundException;
public abstract Response deviceIdAvailableLoadGet(Integer id, Long from, Long to, String resolution)
throws NotFoundException;
public abstract Response deviceIdGet(Integer id) throws NotFoundException;
protected Response getOptionsResponse() {
String allowedOrigin = "";
try {
allowedOrigin = PropertyFileHandler.getInstance().getPropertyValueFromKey("api.cors.allowed");
} catch (IllegalArgumentException | PropertyException | IOException e) {
LOGGER.error("Could not get allowed origin.", e);
}
Response response = Response.ok().header("Allow", "GET").header("Access-Control-Allow-Origin", allowedOrigin)
.header("Access-Control-Allow-Headers", "authorization, content-type").build();
return response;
}
}
public class DeviceApi {
private final DeviceApiService delegate = DeviceApiServiceFactory.getDeviceApi();
// #formatter:off
#GET
#Produces({ "application/json" })
#io.swagger.annotations.ApiOperation(
value = "Get devices",
notes = "",
response = Device.class,
responseContainer = "List",
authorizations = { #io.swagger.annotations.Authorization(value = "basicAuth") },
tags = { "Device", }
)
#io.swagger.annotations.ApiResponses(
value = { #io.swagger.annotations.ApiResponse(
code = 200,
message = "200 OK",
response = Device.class,
responseContainer = "List")
})
public Response deviceGet() throws NotFoundException {
return delegate.deviceGet();
}
#OPTIONS
#Consumes({ "application/json" })
#Produces({ "application/json" })
#io.swagger.annotations.ApiOperation(value = "CORS support", notes = "", response = Void.class, authorizations = {
#io.swagger.annotations.Authorization(value = "basicAuth") }, tags = { "Device", })
#io.swagger.annotations.ApiResponses(value = {
#io.swagger.annotations.ApiResponse(code = 200, message = "Default response for CORS method", response = Void.class) })
public Response deviceOptions() throws NotFoundException {
return delegate.getOptionsResponse();
}
}
EDIT:
This are the headers of the request the swagger ui creates:
Accept: text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: de,en-US;q=0.7,en;q=0.3
Access-Control-Request-Headers: authorization
Access-Control-Request-Method: GET
Connection: keep-alive
DNT: 1
Host: api.myfancyurl.com
Origin: http://apidoc.myfancyurl.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/61.0
It seems that the authorization header is missing. When I edit the request and resend it with the authorization header and encoded credentials it works.
But I don't know why swagger doesn't add this header. Should one accept all options requests without authorization?
I have a REST endpoint #POST where the form params are null when the Content-Type is application/x-www-form-urlencoded. There is a ContainerRequestFilter earlier in the chain (code at the bottom) that takes the request, changes the stream to a BufferedInputStream, and then logs the request. If I remove this logging code, the endpoint has the correct form params. Otherwise, they're null and I can't figure out why.
Now if I use application/json, my endpoint has the correct params regardless if the logger is enabled or disabled.
I need application/x-www-form-urlencoded because the REST endpoint needs to redirect and browsers prevent redirection if the request isn't standard (preflight)
REST Endpoint that isn't working (OAuthRequest has null members)
#Stateless
#Path("v1/oauth2")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Produces(MediaType.APPLICATION_JSON)
public class OAuthTokenResource {
#POST
public Response getToken(#Form OAuthRequest oauthRequest) {
...
}
OAuthRequest
public class OAuthRequest {
#FormParam(OAuthParam.CLIENT_ID)
#JsonProperty(OAuthParam.CLIENT_ID)
private String clientId;
#URL
#FormParam(OAuthParam.REDIRECT_URI)
#JsonProperty(OAuthParam.REDIRECT_URI)
private String redirectUri;
#FormParam(OAuthParam.USERNAME)
private String username;
#FormParam(OAuthParam.PASSWORD)
private String password;
...
}
Logging Filter
#Override
public void filter(final ContainerRequestContext context) throws IOException {
...
if (logEntity && context.hasEntity()) {
context.setEntityStream(logInboundEntity(builder, context.getEntityStream(), context.getMediaType()));
}
logger.debug(builder.toString());
}
private InputStream logInboundEntity(final StringBuilder builder, InputStream stream, MediaType mediaType) throws IOException {
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(maxEntitySize + 1);
final byte[] entity = new byte[maxEntitySize + 1];
final int entitySize = stream.read(entity);
if ( entitySize > 0 ) {
String body = new String(entity, 0, Math.min(entitySize, maxEntitySize), StandardCharsets.UTF_8);
builder.append("\nBody: ");
builder.append(body);
}
if (entitySize > maxEntitySize) {
builder.append(MORE_INDICATOR);
}
stream.reset();
return stream;
}
Okay I am still not sure why #Form and #FormParam does not work if the InputStream is read during the filter chain.
But, I discovered a workaround as follows.
#POST
public Response getToken(MultivaluedMap<String, String> formParams) {
...
}
This provides the same behavior as during application/json as the params are already set even if the InputStream has been consumed.
However ultimately we went with disabling logging of the request body in our filter for security reasons.
I generate proxy classes with wsdl.exe to request web-services, that are probably build at java platform. The problem is with encoding of response. I get '?' instead of russian letters.(for example '????26' instead of 'АН26')
I also use soapUI and everything works well there. I am not experienced at configuring .Net clients. So how I could determine and configure proper encoding for response. I already played with app.config as next:
I attach headers information here. I don't wee encoding info at responce headers...
request headers:
Accept-Encoding: gzip,deflate
Content-Type: text/xml;charset=UTF-8
SOAPAction: "urn:#DCSSci_ListFlight_5"
Content-Length: 641
Host: 109.73.1.66:23022
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
response headers:
HTTP/1.1 200 OK
Date: Thu, 06 Sep 2012 03:47:52 GMT
Server: Apache/2.2.10 (Linux/SUSE)
200 OKX-FidelXML-Version: 2.0
Content-length: 15464
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/xml
Solution:
public class TraceExtension : SoapExtension
{
Stream oldStream;
Stream newStream;
public override Stream ChainStream(Stream stream)
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type WebServiceType)
{
return null;
}
public override void Initialize(object initializer)
{
}
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
newStream.Position = 0;
Copy(newStream, oldStream);
break;
case SoapMessageStage.BeforeDeserialize:
message.ContentType = "application/soap+xml; utf-8";
Copy(oldStream, newStream);
newStream.Position = 0;
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
void Copy(Stream from, Stream to)
{
TextReader reader = new StreamReader(from, System.Text.Encoding.GetEncoding("utf-8"));
TextWriter writer = new StreamWriter(to, System.Text.Encoding.GetEncoding("utf-8"));
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
}
[AttributeUsage(AttributeTargets.Method)]
public class TraceExtensionAttribute : SoapExtensionAttribute
{
private int priority;
public override Type ExtensionType
{
get { return typeof(TraceExtension); }
}
public override int Priority
{
get { return priority; }
set { priority = value; }
}
}
And than just add
[TraceExtension()]
attribute for proxy invoke method
You can override GetWebResponse of your proxy and change the encoding
public class YourProxyClass : SoapHttpClientProtocol
{
protected override WebResponse GetWebResponse(WebRequest request)
{
var response = base.GetWebResponse(request);
response.Headers["Content-Type"] = "text/xml; charset=utf-8"; //<==
return response;
}
}