From dbbf057ea1ebb38d80924b2cbf3b4ae859238d43 Mon Sep 17 00:00:00 2001 From: Maxim Nesen Date: Tue, 3 Oct 2023 11:26:25 +0200 Subject: [PATCH] Expect:100-continue fixes for Netty Signed-off-by: Maxim Nesen --- .../Expect100ContinueConnectorExtension.java | 7 +- .../JerseyExpectContinueHandler.java | 120 +++++++++++++ .../connector/NettyClientProperties.java | 17 ++ .../netty/connector/NettyConnector.java | 46 ++--- .../netty/connector/localization.properties | 3 +- .../netty/httpserver/HttpVersionChooser.java | 4 +- .../netty/httpserver/JerseyServerHandler.java | 6 +- .../httpserver/JerseyServerInitializer.java | 1 + docs/src/main/docbook/appendix-properties.xml | 14 +- docs/src/main/docbook/jersey.ent | 1 + .../nettyconnector/Expect100ContinueTest.java | 160 +++++++++++++++--- tests/integration/jackson-14/pom.xml | 7 +- 12 files changed, 322 insertions(+), 64 deletions(-) create mode 100644 connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java index 5d1e7d2990..471321ff06 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java @@ -19,12 +19,12 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.client.internal.ConnectorExtension; -import javax.ws.rs.HttpMethod; import java.io.IOException; import java.net.ProtocolException; @@ -47,8 +47,9 @@ public void invoke(ClientRequest request, HttpRequest extensionParam) { final boolean allowStreaming = length > expectContinueSizeThreshold || entityProcessing == RequestEntityProcessing.CHUNKED; - if (!Boolean.TRUE.equals(expectContinueActivated) - || !(HttpMethod.POST.equals(request.getMethod()) || HttpMethod.PUT.equals(request.getMethod())) + if (extensionParam.protocolVersion().equals(HttpVersion.HTTP_1_0) + || !Boolean.TRUE.equals(expectContinueActivated) + || !request.hasEntity() || !allowStreaming) { return; } diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java new file mode 100644 index 0000000000..6fc026ff15 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import org.glassfish.jersey.client.ClientRequest; + +import javax.ws.rs.ProcessingException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter { + + private boolean isExpected; + + private static final List statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE, + HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED, + HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE); + + private CompletableFuture expectedFuture = new CompletableFuture<>(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (isExpected && msg instanceof HttpResponse) { + final HttpResponse response = (HttpResponse) msg; + if (statusesToBeConsidered.contains(response.status())) { + expectedFuture.complete(response.status()); + } + if (!HttpResponseStatus.CONTINUE.equals(response.status())) { + ctx.fireChannelRead(msg); //bypass the message to the next handler in line + } else { + ctx.pipeline().remove(this); + } + } else { + if (!isExpected) { + ctx.pipeline().remove(this); + } + ctx.fireChannelRead(msg); //bypass the message to the next handler in line + } + } + + CompletableFuture processExpect100ContinueRequest(HttpRequest nettyRequest, + ClientRequest jerseyRequest, + Channel ch, + Integer timeout) + throws InterruptedException, ExecutionException, TimeoutException { + //check for 100-Continue presence/availability + final Expect100ContinueConnectorExtension expect100ContinueExtension + = new Expect100ContinueConnectorExtension(); + + final DefaultFullHttpRequest nettyRequestHeaders = + new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri()); + nettyRequestHeaders.headers().setAll(nettyRequest.headers()); + //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be + //enriched with the 'Expect:100-continue' header. + expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders); + + final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders)) + // Send only head of the HTTP request enriched with Expect:100-continue header. + ? ch.writeAndFlush(nettyRequestHeaders) + // Expect:100-Continue either is not supported or is turned off + : null; + isExpected = expect100ContinueFuture != null; + if (!isExpected) { + ch.pipeline().remove(this); + } else { + final HttpResponseStatus status = expectedFuture + .get(timeout, TimeUnit.MILLISECONDS); + + processExpectationStatus(status); + } + return expectedFuture; + } + + private void processExpectationStatus(HttpResponseStatus status) + throws TimeoutException { + if (!statusesToBeConsidered.contains(status)) { + throw new ProcessingException(LocalizationMessages + .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null); + } + if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) { + isExpected = false; + throw new TimeoutException(); // continue without expectations + } + if (!HttpResponseStatus.CONTINUE.equals(status)) { + throw new ProcessingException(LocalizationMessages + .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null); + } + } + + boolean isExpected() { + return isExpected; + } +} \ No newline at end of file diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java index 344749b855..9c79d1281d 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java @@ -112,4 +112,21 @@ public class NettyClientProperties { */ public static final String PRESERVE_METHOD_ON_REDIRECT = "jersey.config.client.redirect.preserve.method"; + + /** + * This timeout is used for waiting for 100-Continue response when 100-Continue is sent by the client. + * + * @since 2.41 + */ + public static final String + EXPECT_100_CONTINUE_TIMEOUT = "jersey.config.client.request.expect.100.continue.timeout"; + + /** + * The default value of EXPECT_100_CONTINUE_TIMEOUT. + * + * @since 2.41 + */ + public static final Integer + DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE = 500; + } diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index 6629d334b7..e98d982801 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -31,10 +31,12 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import javax.net.ssl.SSLContext; @@ -46,7 +48,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; @@ -124,6 +125,7 @@ class NettyConnector implements Connector { private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool"; private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler"; private static final String REQUEST_HANDLER = "request_handler"; + private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler"; NettyConnector(Client client) { @@ -190,6 +192,9 @@ public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCa protected void execute(final ClientRequest jerseyRequest, final Set redirectUriHistory, final CompletableFuture responseAvailable) { Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0); + final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty( + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, + NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); if (timeout == null || timeout < 0) { throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout)); } @@ -321,9 +326,11 @@ protected void initChannel(SocketChannel ch) throws Exception { final Channel ch = chan; JerseyClientHandler clientHandler = new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this); + final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler(); // read timeout makes sense really as an inactivity timeout ch.pipeline().addLast(READ_TIMEOUT_HANDLER, new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS)); + ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler); ch.pipeline().addLast(REQUEST_HANDLER, clientHandler); responseDone.whenComplete((_r, th) -> { @@ -408,26 +415,22 @@ public void operationComplete(io.netty.util.concurrent.Future futu // // Set later after the entity is "written" // break; } + try { + expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest, + ch, expect100ContinueTimeout); + } catch (ExecutionException e) { + responseDone.completeExceptionally(e); + } catch (TimeoutException e) { + //Expect:100-continue allows timeouts by the spec + //just removing the pipeline from processing + if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) { + ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER); + } + } - //check for 100-Continue presence/availability - final Expect100ContinueConnectorExtension expect100ContinueExtension - = new Expect100ContinueConnectorExtension(); - - final DefaultFullHttpRequest rq = new DefaultFullHttpRequest(nettyRequest.protocolVersion(), - nettyRequest.method(), nettyRequest.uri()); - rq.headers().setAll(nettyRequest.headers()); - expect100ContinueExtension.invoke(jerseyRequest, rq); - - ChannelFutureListener expect100ContinueListener = null; - ChannelFuture expect100ContinueFuture = null; - - if (HttpUtil.is100ContinueExpected(rq)) { - expect100ContinueListener = - future -> ch.pipeline().writeAndFlush(nettyRequest); - expect100ContinueFuture = ch.pipeline().writeAndFlush(rq).sync().awaitUninterruptibly() - .addListener(expect100ContinueListener); - } else { - // Send the HTTP request. + if (!expect100ContinueHandler.isExpected()) { + // Send the HTTP request. Expect:100-continue processing is not applicable + // in this case. entityWriter.writeAndFlush(nettyRequest); } @@ -443,9 +446,6 @@ public OutputStream getOutputStream(int contentLength) throws IOException { } else { entityWriter.write(entityWriter.getChunkedInput()); } - if (expect100ContinueFuture != null && expect100ContinueListener != null) { - expect100ContinueFuture.removeListener(expect100ContinueListener); - } executorService.execute(new Runnable() { @Override diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties index ba91c4f649..7d6f9fcc39 100644 --- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties +++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved. # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0, which is available at @@ -22,3 +22,4 @@ redirect.no.location="Received redirect that does not contain a location or the redirect.error.determining.location="Error determining redirect location: ({0})." redirect.infinite.loop="Infinite loop in chained redirects detected." redirect.limit.reached="Max chained redirect limit ({0}) exceeded." +unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}"). diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java index 320d7bae32..af52543b38 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -20,6 +20,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; @@ -55,6 +56,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) thr if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { ctx.pipeline().addLast(new HttpServerCodec(), + new HttpServerExpectContinueHandler(), new ChunkedWriteHandler(), new JerseyServerHandler(baseUri, container, resourceConfig)); return; diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java index 0f2a7ae8f0..1576f7e442 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -76,10 +76,6 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { final HttpRequest req = (HttpRequest) msg; - if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); - } - nettyInputStream.clear(); // clearing the content - possible leftover from previous request processing. final ContainerRequest requestContext = createContainerRequest(ctx, req); diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java index 5f4a9595c6..0d1d4247af 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java @@ -96,6 +96,7 @@ public void initChannel(SocketChannel ch) { p.addLast(sslCtx.newHandler(ch.alloc())); } p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); p.addLast(new ChunkedWriteHandler()); p.addLast(new JerseyServerHandler(baseUri, container, resourceConfig)); } diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml index a5c652ac57..80529206cc 100644 --- a/docs/src/main/docbook/appendix-properties.xml +++ b/docs/src/main/docbook/appendix-properties.xml @@ -941,7 +941,7 @@ Property for threshold size for content length after which Expect:100-Continue header would be applied before the main request. - Default threshold size (64kb) after which which Expect:100-Continue header would be applied before + Default threshold size (64kb) after which Expect:100-Continue header would be applied before the main request. Since 2.32 @@ -2062,6 +2062,18 @@ + + &jersey.netty.NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT; + jersey.config.client.request.expect.100.continue.timeout + + + This timeout is used for waiting for 100-Continue response when 100-Continue + is sent by the client. + Default timeout value is 500 ms after which Expect:100-Continue feature is ignored. + Since 2.41 + + + diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent index 25e6b9e260..a6b2d80b21 100644 --- a/docs/src/main/docbook/jersey.ent +++ b/docs/src/main/docbook/jersey.ent @@ -558,6 +558,7 @@ NettyClientProperties.MAX_CONNECTIONS_TOTAL" > NettyClientProperties.MAX_REDIRECTS" > NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT" > +NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT" > NettyConnectorProvider"> ApplicationHandler"> @BackgroundScheduler"> diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java index 9051d3a685..bcb350d16d 100644 --- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java @@ -16,52 +16,88 @@ package org.glassfish.jersey.tests.e2e.client.nettyconnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.client.http.Expect100ContinueFeature; +import org.glassfish.jersey.netty.connector.NettyClientProperties; import org.glassfish.jersey.netty.connector.NettyConnectorProvider; -import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.POST; -import javax.ws.rs.Path; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Application; +import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; +import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; -public class Expect100ContinueTest extends JerseyTest { +public class Expect100ContinueTest /*extends JerseyTest*/ { private static final String RESOURCE_PATH = "expect"; + + private static final String RESOURCE_PATH_NOT_SUPPORTED = "fail417"; + + private static final String RESOURCE_PATH_UNAUTHORIZED = "fail401"; + + private static final String RESOURCE_PATH_PAYLOAD_TOO_LARGE = "fail413"; + + private static final String RESOURCE_PATH_METHOD_NOT_SUPPORTED = "fail405"; + private static final String ENTITY_STRING = "1234567890123456789012345678901234567890123456789012" + "3456789012345678901234567890"; + private static final Integer portNumber = 9997; - @Path(RESOURCE_PATH) - public static class Expect100ContinueResource { + private static Server server; + @BeforeAll + public static void startExpect100ContinueTestServer() { + server = new Server(portNumber); + server.setHandler(new Expect100ContinueTestHandler()); + try { + server.start(); + } catch (Exception e) { - @POST - public Response publishResource(@HeaderParam("Expect") String expect) { - if ("100-Continue".equalsIgnoreCase(expect)) { - return Response.noContent().build(); - } - return Response.ok("TEST").build(); } + } + @AfterAll + public static void stopExpect100ContinueTestServer() { + try { + server.stop(); + } catch (Exception e) { + } + } + + private static Client client; + @BeforeEach + public void beforeEach() { + final ClientConfig config = new ClientConfig(); + this.configureClient(config); + client = ClientBuilder.newClient(config); } - @Override - protected Application configure() { - return new ResourceConfig(Expect100ContinueResource.class); + private Client client() { + return client; + } + + public WebTarget target(String path) { + return client().target(String.format("http://localhost:%d", portNumber)).path(path); } - @Override protected void configureClient(ClientConfig config) { config.connectorProvider(new NettyConnectorProvider()); } @@ -86,15 +122,15 @@ public void testExpect100ContinueBuffered() { .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L) .post(Entity.text(ENTITY_STRING)); - assertEquals(100, response.getStatus(), "Expected 100"); //Expect header sent - No Content response + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response } @Test public void testExpect100ContinueCustomLength() { final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L)) - .request().header(HttpHeaders.CONTENT_LENGTH, 101L) + .request().header(HttpHeaders.CONTENT_LENGTH, Integer.MAX_VALUE) .post(Entity.text(ENTITY_STRING)); - assertEquals(100, response.getStatus(), "Expected 100"); //Expect header sent - No Content response + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response } @Test @@ -123,6 +159,82 @@ public void testExpect100ContinueRegisterViaCustomProperty() { .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) .request().header(HttpHeaders.CONTENT_LENGTH, 44L) .post(Entity.text(ENTITY_STRING)); - assertEquals(100, response.getStatus(), "Expected 100"); //Expect header sent - No Content response + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueNotSupported() { + final Response response = target(RESOURCE_PATH_NOT_SUPPORTED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING)); + assertEquals(417, response.getStatus(), "Expected 417"); //Expectations not supported + } + + @Test + public void testExpect100ContinueUnauthorized() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + @Test + public void testExpect100ContinuePayloadTooLarge() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_PAYLOAD_TOO_LARGE) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + @Test + public void testExpect100ContinueMethodNotSupported() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + static class Expect100ContinueTestHandler extends AbstractHandler { + @Override + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) throws IOException { + boolean expected = request.getHeader("Expect") != null; + boolean failed = false; + if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) { + response.sendError(417); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) { + response.sendError(401); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) { + response.sendError(413); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) { + response.sendError(405); + failed = true; + } + if (expected && !failed) { + System.out.println("Expect:100-continue found, sending response header"); + response.setStatus(204); + } + response.getWriter().println(); + response.flushBuffer(); + baseRequest.setHandled(true); + + request.getReader().lines().forEach(System.out::println); + } } -} +} \ No newline at end of file diff --git a/tests/integration/jackson-14/pom.xml b/tests/integration/jackson-14/pom.xml index a07de08b53..04a31ce8de 100644 --- a/tests/integration/jackson-14/pom.xml +++ b/tests/integration/jackson-14/pom.xml @@ -112,11 +112,6 @@ hamcrest test - - org.junit.jupiter - junit-jupiter - test - - + \ No newline at end of file