diff --git a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java index 7d9ffbace50..bdc6b99a58e 100644 --- a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java +++ b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.common.http.Http.DateTime; import io.helidon.common.http.Http.HeaderValue; +import io.helidon.common.media.type.ParserMode; import static io.helidon.common.http.Http.Header.ACCEPT_PATCH; import static io.helidon.common.http.Http.Header.EXPIRES; @@ -36,12 +37,24 @@ public interface ClientResponseHeaders extends Headers { /** * Create a new instance from headers parsed from client response. + * Strict media type parsing mode is used for {@code Content-Type} header. * * @param responseHeaders client response headers * @return immutable instance of client response HTTP headers */ static ClientResponseHeaders create(Headers responseHeaders) { - return new ClientResponseHeadersImpl(responseHeaders); + return new ClientResponseHeadersImpl(responseHeaders, ParserMode.STRICT); + } + + /** + * Create a new instance from headers parsed from client response. + * + * @param responseHeaders client response headers + * @param parserMode media type parsing mode + * @return immutable instance of client response HTTP headers + */ + static ClientResponseHeaders create(Headers responseHeaders, ParserMode parserMode) { + return new ClientResponseHeadersImpl(responseHeaders, parserMode); } /** diff --git a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java index 8197bb5e3b4..82e1a882bdc 100644 --- a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java +++ b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,18 @@ import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; +import io.helidon.common.media.type.ParserMode; + class ClientResponseHeadersImpl implements ClientResponseHeaders { private final Headers headers; + private final ParserMode parserMode; - ClientResponseHeadersImpl(Headers headers) { + ClientResponseHeadersImpl(Headers headers, ParserMode parserMode) { this.headers = headers; + this.parserMode = parserMode; } @Override @@ -47,6 +52,16 @@ public Http.HeaderValue get(Http.HeaderName name) { return headers.get(name); } + @Override + public Optional contentType() { + if (parserMode == ParserMode.RELAXED) { + return contains(HeaderEnum.CONTENT_TYPE) + ? Optional.of(HttpMediaType.create(get(HeaderEnum.CONTENT_TYPE).value(), parserMode)) + : Optional.empty(); + } + return headers.contentType(); + } + @Override public int size() { return headers.size(); diff --git a/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java b/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java index b5e29d7383e..970147e9d60 100644 --- a/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java +++ b/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java b/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java index d7dfadaf817..b846ebb57a5 100644 --- a/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java +++ b/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; +import io.helidon.common.media.type.ParserMode; /** * Media type used in HTTP headers, in addition to the media type definition, these may contain additional @@ -106,12 +107,24 @@ static HttpMediaType create(MediaType mediaType) { /** * Parse media type from the provided string. + * Strict media type parsing mode is used. * * @param mediaTypeString media type string * @return HTTP media type parsed from the string */ static HttpMediaType create(String mediaTypeString) { - return Builder.parse(mediaTypeString); + return Builder.parse(mediaTypeString, ParserMode.STRICT); + } + + /** + * Parse media type from the provided string. + * + * @param mediaTypeString media type string + * @param parserMode media type parsing mode + * @return HTTP media type parsed from the string + */ + static HttpMediaType create(String mediaTypeString, ParserMode parserMode) { + return Builder.parse(mediaTypeString, parserMode); } /** @@ -310,7 +323,7 @@ MediaType mediaType() { return mediaType; } - private static HttpMediaType parse(String mediaTypeString) { + private static HttpMediaType parse(String mediaTypeString, ParserMode parserMode) { // text/plain; charset=UTF-8 Builder b = builder(); @@ -337,7 +350,7 @@ private static HttpMediaType parse(String mediaTypeString) { } } } else { - b.mediaType(MediaTypes.create(mediaTypeString)); + b.mediaType(MediaTypes.create(mediaTypeString, parserMode)); } return b.build(); } diff --git a/common/http/src/test/java/io/helidon/common/http/Http1HeadersParserTest.java b/common/http/src/test/java/io/helidon/common/http/Http1HeadersParserTest.java index b76f71d49cf..d7b6b0f71a6 100644 --- a/common/http/src/test/java/io/helidon/common/http/Http1HeadersParserTest.java +++ b/common/http/src/test/java/io/helidon/common/http/Http1HeadersParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/http/src/test/java/io/helidon/common/http/MediaTypeTest.java b/common/http/src/test/java/io/helidon/common/http/MediaTypeTest.java index 9dfa09a4f38..a71910f942b 100644 --- a/common/http/src/test/java/io/helidon/common/http/MediaTypeTest.java +++ b/common/http/src/test/java/io/helidon/common/http/MediaTypeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; +import io.helidon.common.media.type.ParserMode; import org.junit.jupiter.api.Test; @@ -30,6 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableWithSize.iterableWithSize; import static org.hamcrest.number.IsCloseTo.closeTo; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Unit test for {@link MediaType}. @@ -134,4 +136,21 @@ void testBuilt() { assertThat(mediaType.parameters(), is(Map.of("q", "0.1", "charset", "ISO-8859-2"))); assertThat(mediaType.qualityFactor(), closeTo(0.1, 0.000001)); } + + // Calling create method with "text" argument shall throw IllegalArgumentException in strict mode. + @Test + void parseInvalidTextInStrictMode() { + assertThrows(IllegalArgumentException.class, () -> { + HttpMediaType.create("text"); + }, + "Cannot parse media type: text"); + } + + // Calling create method with "text" argument shall return "text/plain" in relaxed mode. + @Test + void parseInvalidTextInRelaxedMode() { + HttpMediaType type = HttpMediaType.create("text", ParserMode.RELAXED); + assertThat(type.text(), is("text/plain")); + } + } diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java index 589beaa456f..311c49b3553 100644 --- a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java +++ b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,25 @@ package io.helidon.common.media.type; +import java.util.Optional; + record MediaTypeImpl(String type, String subtype, String text) implements MediaType { - static MediaType parse(String fullType) { + + private static final System.Logger LOGGER = System.getLogger(MediaTypeImpl.class.getName()); + + static MediaType parse(String fullType, ParserMode parserMode) { int slashIndex = fullType.indexOf('/'); if (slashIndex < 1) { + if (parserMode == ParserMode.RELAXED) { + Optional maybeRelaxedType = ParserMode.findRelaxedMediaType(fullType); + if (maybeRelaxedType.isPresent()) { + LOGGER.log(System.Logger.Level.DEBUG, + () -> String.format("Invalid media type value \"%s\" replaced with \"%s\"", + fullType, + maybeRelaxedType.get().text())); + return maybeRelaxedType.get(); + } + } throw new IllegalArgumentException("Cannot parse media type: " + fullType); } return new MediaTypeImpl(fullType.substring(0, slashIndex), diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java index adcb2f37d0b..66fe780fdb1 100644 --- a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java +++ b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,13 +151,26 @@ public static MediaType create(String type, String subtype) { /** * Create a new media type from the full media type string. + * Strict media type parsing mode is used. * * @param fullType media type string, such as {@code application/json} * @return media type for the string */ public static MediaType create(String fullType) { MediaTypeEnum types = MediaTypeEnum.find(fullType); - return types == null ? MediaTypeImpl.parse(fullType) : types; + return types == null ? MediaTypeImpl.parse(fullType, ParserMode.STRICT) : types; + } + + /** + * Create a new media type from the full media type string. + * + * @param fullType media type string, such as {@code application/json} + * @param parserMode media type parsing mode + * @return media type for the string + */ + public static MediaType create(String fullType, ParserMode parserMode) { + MediaTypeEnum types = MediaTypeEnum.find(fullType); + return types == null ? MediaTypeImpl.parse(fullType, parserMode) : types; } /** diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java b/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java new file mode 100644 index 00000000000..8f3dfcdd4cc --- /dev/null +++ b/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.media.type; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Media type parsing mode. + */ +public enum ParserMode { + + /** + * Strict mode (default). + * Media type must match known name. + */ + STRICT, + /** + * Relaxed mode. + * Apply additional rules to identify unknown media types. + */ + RELAXED; + + // Relaxed media types mapping + private static final Map RELAXED_TYPES = Map.of( + "text", MediaTypes.TEXT_PLAIN // text -> text/plain + ); + + /** + * Find relaxed media type mapping for provided value. + * + * @param value source media type value + * @return mapped media type value or {@code Optional.empty()} + * when no mapping for given value exists + */ + static Optional findRelaxedMediaType(String value) { + Objects.requireNonNull(value); + MediaType relaxedValue = RELAXED_TYPES.get(value); + return relaxedValue != null ? Optional.of(relaxedValue) : Optional.empty(); + } + +} diff --git a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java index 36fd23396dd..cedd0599837 100644 --- a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java +++ b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java index 4346d7fa6d0..179ab9109b2 100644 --- a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java +++ b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/nima/tests/integration/webclient/webclient/pom.xml b/nima/tests/integration/webclient/webclient/pom.xml index 891f5101a68..887b7ac3dcd 100644 --- a/nima/tests/integration/webclient/webclient/pom.xml +++ b/nima/tests/integration/webclient/webclient/pom.xml @@ -36,6 +36,11 @@ io.helidon.nima.webclient helidon-nima-webclient + + io.helidon.nima.webserver + helidon-nima-webserver + test + io.helidon.nima.testing.junit5 helidon-nima-testing-junit5-webserver diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java new file mode 100644 index 00000000000..c09b086cfe5 --- /dev/null +++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.webclient.http1; + +import java.util.Optional; + +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.common.http.HttpMediaType; +import io.helidon.common.media.type.ParserMode; +import io.helidon.nima.webclient.ClientResponse; +import io.helidon.nima.webclient.HttpClient; +import io.helidon.nima.webclient.WebClient; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class HeadersTest { + + private static WebServer server; + + @BeforeAll + static void beforeAll() { + server = WebServer.builder() + .routing(routing -> routing.register("/test", new TestService()).build()) + .start(); + } + + @AfterAll + static void afterAll() { + server.stop(); + } + + // Verify that invalid content type is present in response headers and is accesible + @Test + public void testInvalidContentType() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .build(); + try (ClientResponse res = client.method(Http.Method.GET) + .path("/invalidContentType") + .request()) { + Headers h = res.headers(); + Http.HeaderValue contentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(contentType.value(), is(TestService.INVALID_CONTENT_TYPE_VALUE)); + } + } + + // Verify that "Content-Type: text" header parsing fails in strict mode + @Test + public void testInvalidTextContentTypeStrict() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .build(); + ClientResponse res = client.method(Http.Method.GET) + .path("/invalidTextContentType") + .request(); + assertThat(res.status(), is(Http.Status.OK_200)); + Headers h = res.headers(); + // Raw protocol data value + Http.HeaderValue rawContentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(rawContentType.value(), is(TestService.INVALID_CONTENT_TYPE_TEXT)); + // Media type parsed value is invalid, IllegalArgumentException shall be thrown + try { + h.contentType(); + Assertions.fail("Content-Type: text parsing must throw an exception in strict mode"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), is("Cannot parse media type: text")); + } + } + + // Verify that "Content-Type: text" header parsing returns text/plain in relaxed mode + @Test + public void testInvalidTextContentTypeRelaxed() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .mediaTypeParserMode(ParserMode.RELAXED) + .build(); + ClientResponse res = client.method(Http.Method.GET) + .path("/invalidTextContentType") + .request(); + assertThat(res.status(), is(Http.Status.OK_200)); + Headers h = res.headers(); + // Raw protocol data value + Http.HeaderValue rawContentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(rawContentType.value(), is(TestService.INVALID_CONTENT_TYPE_TEXT)); + // Media type parsed value + Optional contentType = h.contentType(); + assertThat(contentType.isPresent(), is(true)); + assertThat(contentType.get().text(), is(TestService.RELAXED_CONTENT_TYPE_TEXT)); + } + + static final class TestService implements HttpService { + + TestService() { + } + + @Override + public void routing(HttpRules rules) { + rules + .get("/invalidContentType", this::invalidContentType) + .get("/invalidTextContentType", this::invalidTextContentType); + } + + private static final String INVALID_CONTENT_TYPE_VALUE = "invalid header value"; + + private void invalidContentType(ServerRequest request, ServerResponse response) { + response.header(Http.Header.CONTENT_TYPE, INVALID_CONTENT_TYPE_VALUE) + .send(); + } + + private static final String INVALID_CONTENT_TYPE_TEXT = "text"; + private static final String RELAXED_CONTENT_TYPE_TEXT = "text/plain"; + + // Returns Content-Type: text instead of text/plain + private void invalidTextContentType(ServerRequest request, ServerResponse response) { + response.header(Http.Header.CONTENT_TYPE, INVALID_CONTENT_TYPE_TEXT) + .send(); + } + + } + +} diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java index e186705f4f8..cb9b34af958 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java @@ -20,6 +20,7 @@ import java.util.Optional; import io.helidon.common.http.Headers; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.config.metadata.ConfiguredOption; import io.helidon.nima.common.tls.Tls; @@ -90,4 +91,13 @@ public interface ClientConfig { */ Headers defaultHeaders(); + /** + * Client {@code Content-Type} header matching mode. + * Supported values are {@code ParserMode.STRICT} and {@code ParserMode.RELAXED}. + * + * @return {@code Content-Type} header matching mode + */ + @ConfiguredOption("STRICT") + ParserMode mediaTypeParserMode(); + } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java index b3e7cf974cc..12651692276 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java @@ -22,6 +22,7 @@ import io.helidon.common.LazyValue; import io.helidon.common.http.Headers; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webclient.spi.DnsResolver; @@ -45,6 +46,7 @@ public class LoomClient implements WebClient { private final int maxRedirects; private final boolean followRedirects; private final Headers defaultHeaders; + private final ParserMode mediaTypeParserMode; /** * Construct this instance from a subclass of builder. @@ -60,6 +62,7 @@ protected LoomClient(WebClient.Builder builder) { this.maxRedirects = builder.maxRedirect(); this.followRedirects = builder.followRedirect(); this.defaultHeaders = builder.defaultHeaders(); + this.mediaTypeParserMode = builder.mediaTypeParserMode(); } /** @@ -143,4 +146,13 @@ public Headers defaultHeaders() { return defaultHeaders; } + /** + * Media type parsing mode for HTTP {@code Content-Type} header. + * + * @return media type parsing mode + */ + protected ParserMode mediaTypeParserMode() { + return mediaTypeParserMode; + } + } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java index 03f350b9c7b..d8941541f5e 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java @@ -25,6 +25,7 @@ import io.helidon.common.http.ClientRequestHeaders; import io.helidon.common.http.Http; import io.helidon.common.http.WritableHeaders; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webclient.http1.Http1; @@ -73,6 +74,7 @@ abstract class Builder, C extends WebClient> implements private boolean followRedirect; private int maxRedirect; private WritableHeaders defaultHeaders = WritableHeaders.create(); + private ParserMode mediaTypeParserMode = ParserMode.STRICT; /** * Common builder base for all the client builder. @@ -231,6 +233,17 @@ protected B removeHeader(Http.HeaderName name) { return identity(); } + /** + * Configure media type parsing mode for HTTP {@code Content-Type} header. + * + * @param mode media type parsing mode + * @return updated builder instance + */ + public B mediaTypeParserMode(ParserMode mode) { + this.mediaTypeParserMode = mode; + return identity(); + } + /** * Channel options. * @@ -278,5 +291,9 @@ protected WritableHeaders defaultHeaders() { return defaultHeaders; } + protected ParserMode mediaTypeParserMode() { + return this.mediaTypeParserMode; + } + } } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java index 6a99f0c6263..1fd43e781c9 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java @@ -361,6 +361,7 @@ private ClientResponseImpl invokeServices(WebClientService.Chain callChain, serviceResponse.connection(), serviceResponse.reader(), mediaContext, + clientConfig.mediaTypeParserMode(), complete); } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java index d3118336609..62e8ef01d98 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java @@ -34,6 +34,7 @@ import io.helidon.common.http.Http.HeaderValues; import io.helidon.common.http.Http1HeadersParser; import io.helidon.common.http.WritableHeaders; +import io.helidon.common.media.type.ParserMode; import io.helidon.nima.http.encoding.ContentDecoder; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; @@ -67,6 +68,8 @@ class ClientResponseImpl implements Http1ClientResponse { private final CompletableFuture whenComplete; private final boolean hasTrailers; private final List trailerNames; + // Media type parsing mode configured on client. + private final ParserMode parserMode; private ClientConnection connection; private long entityLength; @@ -79,6 +82,7 @@ class ClientResponseImpl implements Http1ClientResponse { ClientConnection connection, DataReader reader, MediaContext mediaContext, + ParserMode parserMode, CompletableFuture whenComplete) { this.responseStatus = responseStatus; this.requestHeaders = requestHeaders; @@ -86,6 +90,7 @@ class ClientResponseImpl implements Http1ClientResponse { this.connection = connection; this.reader = reader; this.mediaContext = mediaContext; + this.parserMode = parserMode; this.channelId = connection.channelId(); this.whenComplete = whenComplete; diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java index 5a4f168ec0e..6c78b54e878 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java @@ -24,6 +24,7 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.http.media.MediaContext; @@ -273,6 +274,18 @@ public Http1ClientBuilder relativeUris(boolean relativeUris) { configBuilder.relativeUris(relativeUris); return this; } + + /** + * Configure media type parsing mode for HTTP {@code Content-Type} header. + * + * @param mode media type parsing mode + * @return updated builder instance + */ + public Http1ClientBuilder mediaTypeParserMode(ParserMode mode) { + configBuilder.mediaTypeParserMode(mode); + return this; + } + } } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java index c400669379c..98f814eb560 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java @@ -95,7 +95,7 @@ ClientResponseHeaders readHeaders(DataReader reader) { clientConfig.maxHeaderSize(), clientConfig.validateHeaders()); - return ClientResponseHeaders.create(writable); + return ClientResponseHeaders.create(writable, clientConfig.mediaTypeParserMode()); } private ClientConnection obtainConnection(WebClientServiceRequest request) { diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java index e0e75fc05b3..d59701aee24 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.