Skip to content

Commit

Permalink
vert-x3#2663 Add params to Accept header
Browse files Browse the repository at this point in the history
  • Loading branch information
komape committed Oct 30, 2024
1 parent 768bca1 commit 2ea9146
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 25 deletions.
7 changes: 7 additions & 0 deletions vertx-web/src/main/java/io/vertx/ext/web/MIMEHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ public interface MIMEHeader extends ParsedHeaderValue {
*/
String mediaType();

/**
* Gets the MIME media type string with parameters attached.
* This includes both the component and subcomponent parts of the MIME type, and parameters.
* @return The MIME media type string.
*/
String mediaTypeWithParams();

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import io.vertx.ext.web.MIMEHeader;

import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class ParsableMIMEValue extends ParsableHeaderValue implements MIMEHeader {

private String component;
Expand Down Expand Up @@ -33,6 +37,19 @@ public String mediaType() {
return component + "/" + subComponent;
}

@Override
public String mediaTypeWithParams() {
ensureHeaderProcessed();
StringBuilder sb = new StringBuilder(component + "/" + subComponent);
if (!parameters().isEmpty()) {
sb.append("; ");
sb.append(parameters().entrySet().stream().map(ParsableMIMEValue::encodeParam).collect(Collectors.joining("; ")));
}

return sb.toString();
}


@Override
protected boolean isMatchedBy2(ParsableHeaderValue matchTry) {
ParsableMIMEValue myMatchTry = (ParsableMIMEValue) matchTry;
Expand Down Expand Up @@ -83,4 +100,39 @@ private void setSubComponent(String subComponent) {
protected int weightedOrderPart2() {
return orderWeight;
}

private static final Pattern SPECIAL_CHARACTERS_IN_PARAM_VALUE = Pattern.compile(".*[\\s,;=].*");

/**
* Encodes a MIME parameter.
* <p>
* This method takes a parameter entry and encodes it into a string format suitable for MIME headers.
* If the parameter value is null, it returns just the key.
* If the value is enclosed in double quotes, it returns the key and value as is.
* If the value contains special characters, it encloses the value in double quotes.
* Otherwise, it returns the key and value in a key=value format.
* @param param The parameter entry to encode.
* @return The encoded parameter string.
*/
private static String encodeParam(Map.Entry<String, String> param) {
// Check if the parameter value is null
if (param.getValue() == null) {
// Return just the key if value is null
return param.getKey();
} else {
String value = param.getValue();
// Check if the value is enclosed in double quotes
if (value.startsWith("\"") && value.endsWith("\"")) {
// Return the key and value as is
return param.getKey() + "=" + value;
// Check if the value contains special characters
} else if (SPECIAL_CHARACTERS_IN_PARAM_VALUE.matcher(value).matches()) {
// Enclose the value in double quotes
return param.getKey() + "=\"" + value + "\"";
} else {
// Return the key and value in key=value format
return param.getKey() + "=" + param.getValue();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ private void checkHandleNoMatch() {
} else if (this.request().method() != HttpMethod.HEAD && matchFailure == 415) {
// In case of a 415, send a header with the accepted content types
this.response()
.putHeader(HttpHeaderNames.ACCEPT, allowedContentTypes.stream().map(MIMEHeader::mediaType).collect(Collectors.joining(", ")))
.putHeader(HttpHeaderNames.ACCEPT,
allowedContentTypes.stream()
.map(MIMEHeader::mediaTypeWithParams)
.collect(Collectors.joining(", ")))
.end();
} else {
this.response().end();
Expand Down
43 changes: 19 additions & 24 deletions vertx-web/src/test/java/io/vertx/ext/web/tests/RouterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1057,17 +1057,17 @@ public void testConsumesWithParameterKey() throws Exception {
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo", res.getHeader("Accept")));
res -> assertEquals("text/html; boo", res.getHeader("Accept")));
}

@Test
public void testConsumesWithParameter() throws Exception {
router.route().consumes("text/html;boo=ya").handler(rc -> rc.response().end());
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=ya", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=ya", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=ya", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=ya", res.getHeader("Accept")));
}

@Test
Expand All @@ -1076,28 +1076,28 @@ public void testConsumesWithQuotedParameterWithComma() throws Exception {
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\";itWorks=4real", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\"", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right;itWorks=4real\"", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah,right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah,right\"", res.getHeader("Accept")));
// this might look wrong but since there is only 1 entry per content-type, the comma has no semantic meaning
// therefore it is ignored
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=yeah,right", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah,right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah,right\"", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah,right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah,right\"", res.getHeader("Accept")));
}

@Test
public void testConsumesWithQuotedParameterWithQuotes() throws Exception {
router.route().consumes("text/html;boo=\"yeah\\\"right\"").handler(rc -> rc.response().end());
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah\\\"right\"", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\"", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah\\\"right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah\\\"right\"", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=yeah,right", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah\\\"right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah\\\"right\"", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah\\\"right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah\\\"right\"", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type",
res -> assertEquals("text/html;boo=\"yeah\\\"right\"", res.getHeader("Accept")));
res -> assertEquals("text/html; boo=\"yeah\\\"right\"", res.getHeader("Accept")));
}

@Test
Expand Down Expand Up @@ -1131,11 +1131,11 @@ public void testConsumesVariableParameters() throws Exception {
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo;works", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=done;it=works", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;yes=no;right", 415, "Unsupported Media Type",
res -> assertEquals("text/html;works", res.getHeader("Accept")));
res -> assertEquals("text/html; boo; works", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/book;boo", 415, "Unsupported Media Type",
res -> assertEquals("text/html;works", res.getHeader("Accept")));
res -> assertEquals("text/html; boo; works", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "text/book;works=aright", 415, "Unsupported Media Type",
res -> assertEquals("text/html;works", res.getHeader("Accept")));
res -> assertEquals("text/html; boo; works", res.getHeader("Accept")));
}

@Test
Expand All @@ -1146,7 +1146,7 @@ public void testConsumesMissingSlash() throws Exception {
testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 200, "OK");
testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type",
res -> assertEquals("json", res.getHeader("Accept")));
res -> assertEquals("*/json", res.getHeader("Accept")));
}

@Test
Expand Down Expand Up @@ -2822,8 +2822,7 @@ public void testUnsupportedMediaTypeCustomErrorHandler() throws Exception {
router.route().consumes("text/html").handler(rc -> rc.response().end());
router.errorHandler(415, context -> context.response().setStatusCode(context.statusCode()).setStatusMessage("Dumb").end());

testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, "Dumb",
res -> assertEquals("text/html", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, "Dumb");
}

@Test
Expand Down Expand Up @@ -2860,8 +2859,7 @@ public void testUnsupportedMediaTypeStatusCode() throws Exception {
router.route().consumes("text/html").handler(rc -> rc.response().end());
router.get("/hello").consumes("something/html").handler(rc -> rc.response().end());

testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415,
HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE.reasonPhrase(), res -> assertEquals("text/html", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE.reasonPhrase());
}

@Test
Expand Down Expand Up @@ -2932,11 +2930,9 @@ public void testRouteMatchingUnsupportedMediaType() throws Exception {
router.post("/api").consumes("application/json").handler(rc -> rc.response().setStatusMessage("post api").end());
router.put("/api").consumes("application/json").handler(rc -> rc.response().setStatusMessage("put api").end());
testRequestWithContentType(HttpMethod.POST, "/api", "application/json", 200, "post api");
testRequestWithContentType(HttpMethod.POST, "/api", "application/xml", 415, "Unsupported Media Type",
res -> assertEquals("application/json", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.POST, "/api", "application/xml", 415, "Unsupported Media Type");
testRequestWithContentType(HttpMethod.PUT, "/api", "application/json", 200, "put api");
testRequestWithContentType(HttpMethod.PUT, "/api", "application/xml", 415, "Unsupported Media Type",
res -> assertEquals("application/json", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.PUT, "/api", "application/xml", 415, "Unsupported Media Type");
testRequestWithContentType(HttpMethod.PATCH, "/api", "application/json", 405, "Method Not Allowed");
}

Expand All @@ -2947,8 +2943,7 @@ public void testRouteMatchingUnsupportedMediaTypeOrder() throws Exception {
router.get("/api").handler(rc -> rc.response().setStatusMessage("get api").end());
router.put("/api").consumes("application/xml").handler(rc -> rc.response().setStatusMessage("put api xml").end());
testRequestWithContentType(HttpMethod.POST, "/api", "application/json", 200, "post api");
testRequestWithContentType(HttpMethod.POST, "/api", "application/xml", 415, "Unsupported Media Type",
res -> assertEquals("application/json", res.getHeader("Accept")));
testRequestWithContentType(HttpMethod.POST, "/api", "application/xml", 415, "Unsupported Media Type");
testRequestWithContentType(HttpMethod.PUT, "/api", "application/json", 200, "put api");
testRequestWithContentType(HttpMethod.PUT, "/api", "application/xml", 200, "put api xml");
testRequestWithContentType(HttpMethod.PATCH, "/api", "application/json", 405, "Method Not Allowed");
Expand Down

0 comments on commit 2ea9146

Please sign in to comment.