11/*
2- * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
2+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates. All rights reserved.
33 *
44 * This program and the accompanying materials are made available under the
55 * terms of the Eclipse Public License v. 2.0, which is available at
1616
1717package org .glassfish .jersey .netty .connector ;
1818
19- import io .netty .channel .Channel ;
20- import io .netty .channel .ChannelFuture ;
2119import io .netty .channel .ChannelHandlerContext ;
2220import io .netty .channel .ChannelInboundHandlerAdapter ;
23- import io .netty .handler .codec .http .DefaultFullHttpRequest ;
24- import io .netty .handler .codec .http .HttpHeaderNames ;
25- import io .netty .handler .codec .http .HttpRequest ;
21+ import io .netty .handler .codec .http .FullHttpMessage ;
2622import io .netty .handler .codec .http .HttpResponse ;
2723import io .netty .handler .codec .http .HttpResponseStatus ;
28- import io .netty .handler .codec .http .HttpUtil ;
29- import org .glassfish .jersey .client .ClientRequest ;
24+ import io .netty .handler .codec .http .LastHttpContent ;
3025
3126import javax .ws .rs .ProcessingException ;
27+ import java .io .IOException ;
28+ import java .util .ArrayList ;
3229import java .util .Arrays ;
3330import java .util .List ;
34- import java .util .concurrent .CompletableFuture ;
35- import java .util .concurrent .ExecutionException ;
36- import java .util .concurrent .TimeUnit ;
31+ import java .util .concurrent .CountDownLatch ;
3732import java .util .concurrent .TimeoutException ;
3833
3934public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter {
4035
41- private boolean isExpected ;
36+ private ExpectationState currentState = ExpectationState . IDLE ;
4237
43- private static final List <HttpResponseStatus > statusesToBeConsidered = Arrays .asList (HttpResponseStatus .CONTINUE ,
44- HttpResponseStatus .UNAUTHORIZED , HttpResponseStatus .EXPECTATION_FAILED ,
45- HttpResponseStatus .METHOD_NOT_ALLOWED , HttpResponseStatus .REQUEST_ENTITY_TOO_LARGE );
38+ private static final List <HttpResponseStatus > finalErrorStatuses = Arrays .asList (HttpResponseStatus .UNAUTHORIZED ,
39+ HttpResponseStatus .REQUEST_ENTITY_TOO_LARGE );
40+ private static final List <HttpResponseStatus > reSendErrorStatuses = Arrays .asList (
41+ HttpResponseStatus .METHOD_NOT_ALLOWED ,
42+ HttpResponseStatus .EXPECTATION_FAILED );
4643
47- private CompletableFuture <HttpResponseStatus > expectedFuture = new CompletableFuture <>();
44+ private static final List <HttpResponseStatus > statusesToBeConsidered = new ArrayList <>(reSendErrorStatuses );
45+
46+ static {
47+ statusesToBeConsidered .addAll (finalErrorStatuses );
48+ statusesToBeConsidered .add (HttpResponseStatus .CONTINUE );
49+ }
50+
51+ private HttpResponseStatus status = null ;
52+
53+ private CountDownLatch latch = null ;
54+
55+ private boolean propagateLastMessage = false ;
4856
4957 @ Override
5058 public void channelRead (ChannelHandlerContext ctx , Object msg ) throws Exception {
51- if (isExpected && msg instanceof HttpResponse ) {
52- final HttpResponse response = (HttpResponse ) msg ;
53- if (statusesToBeConsidered .contains (response .status ())) {
54- expectedFuture .complete (response .status ());
55- }
56- if (!HttpResponseStatus .CONTINUE .equals (response .status ())) {
59+
60+ if (checkExpectResponse (msg ) || checkInvalidExpect (msg )) {
61+ currentState = ExpectationState .AWAITING ;
62+ }
63+ switch (currentState ) {
64+ case AWAITING :
65+ final HttpResponse response = (HttpResponse ) msg ;
66+ status = response .status ();
67+ boolean handshakeDone = processErrorStatuses (status ) || msg instanceof FullHttpMessage ;
68+ currentState = (handshakeDone ) ? ExpectationState .IDLE : ExpectationState .FINISHING ;
69+ processLatch ();
70+ return ;
71+ case FINISHING :
72+ if (msg instanceof LastHttpContent ) {
73+ currentState = ExpectationState .IDLE ;
74+ if (propagateLastMessage ) {
75+ propagateLastMessage = false ;
76+ ctx .writeAndFlush (LastHttpContent .EMPTY_LAST_CONTENT );
77+ }
78+ }
79+ return ;
80+ default :
5781 ctx .fireChannelRead (msg ); //bypass the message to the next handler in line
58- } else {
59- ctx .pipeline ().remove (JerseyExpectContinueHandler .class );
60- }
61- } else {
62- if (!isExpected ) {
63- ctx .pipeline ().remove (JerseyExpectContinueHandler .class );
64- }
65- ctx .fireChannelRead (msg ); //bypass the message to the next handler in line
6682 }
6783 }
6884
69- CompletableFuture <HttpResponseStatus > processExpect100ContinueRequest (HttpRequest nettyRequest ,
70- ClientRequest jerseyRequest ,
71- Channel ch ,
72- Integer timeout )
73- throws InterruptedException , ExecutionException , TimeoutException {
74- //check for 100-Continue presence/availability
75- final Expect100ContinueConnectorExtension expect100ContinueExtension
76- = new Expect100ContinueConnectorExtension ();
77-
78- final DefaultFullHttpRequest nettyRequestHeaders =
79- new DefaultFullHttpRequest (nettyRequest .protocolVersion (), nettyRequest .method (), nettyRequest .uri ());
80- nettyRequestHeaders .headers ().setAll (nettyRequest .headers ());
81-
82- if (!nettyRequestHeaders .headers ().contains (HttpHeaderNames .HOST )) {
83- nettyRequestHeaders .headers ().add (HttpHeaderNames .HOST , jerseyRequest .getUri ().getHost ());
85+ private boolean checkExpectResponse (Object msg ) {
86+ if (currentState == ExpectationState .IDLE && latch != null && msg instanceof HttpResponse ) {
87+ return statusesToBeConsidered .contains (((HttpResponse ) msg ).status ());
8488 }
89+ return false ;
90+ }
91+
92+ private boolean checkInvalidExpect (Object msg ) {
93+ return (ExpectationState .IDLE .equals (currentState )
94+ && msg instanceof HttpResponse
95+ && (HttpResponseStatus .CONTINUE .equals (((HttpResponse ) msg ).status ())
96+ || reSendErrorStatuses .contains (((HttpResponse ) msg ).status ()))
97+ );
98+ }
8599
86- //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be
87- //enriched with the 'Expect:100-continue' header.
88- expect100ContinueExtension .invoke (jerseyRequest , nettyRequestHeaders );
89-
90- final ChannelFuture expect100ContinueFuture = (HttpUtil .is100ContinueExpected (nettyRequestHeaders ))
91- // Send only head of the HTTP request enriched with Expect:100-continue header.
92- ? ch .writeAndFlush (nettyRequestHeaders )
93- // Expect:100-Continue either is not supported or is turned off
94- : null ;
95- isExpected = expect100ContinueFuture != null ;
96- if (!isExpected ) {
97- ch .pipeline ().remove (JerseyExpectContinueHandler .class );
98- } else {
99- final HttpResponseStatus status = expectedFuture
100- .get (timeout , TimeUnit .MILLISECONDS );
101-
102- processExpectationStatus (status );
100+ boolean processErrorStatuses (HttpResponseStatus status ) {
101+ if (reSendErrorStatuses .contains (status )) {
102+ propagateLastMessage = true ;
103103 }
104- return expectedFuture ;
104+ return ( finalErrorStatuses . contains ( status )) ;
105105 }
106106
107- private void processExpectationStatus (HttpResponseStatus status )
108- throws TimeoutException {
107+ void processExpectationStatus ()
108+ throws TimeoutException , IOException {
109+ if (status == null ) {
110+ throw new TimeoutException (); // continue without expectations
111+ }
109112 if (!statusesToBeConsidered .contains (status )) {
110113 throw new ProcessingException (LocalizationMessages
111114 .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES (status .code ()), null );
112115 }
113- if (!expectedFuture .isDone () || HttpResponseStatus .EXPECTATION_FAILED .equals (status )) {
114- isExpected = false ;
115- throw new TimeoutException (); // continue without expectations
116+
117+ if (finalErrorStatuses .contains (status )) {
118+ throw new IOException (LocalizationMessages
119+ .EXPECT_100_CONTINUE_FAILED_REQUEST_FAILED (), null );
116120 }
117- if (!HttpResponseStatus .CONTINUE .equals (status )) {
118- throw new ProcessingException (LocalizationMessages
119- .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES (status .code ()), null );
121+
122+ if (reSendErrorStatuses .contains (status )) {
123+ throw new TimeoutException (LocalizationMessages
124+ .EXPECT_100_CONTINUE_FAILED_REQUEST_SHOULD_BE_RESENT ()); // Re-send request without expectations
125+ }
126+
127+ }
128+
129+ void resetHandler () {
130+ latch = null ;
131+ }
132+
133+ void attachCountDownLatch (CountDownLatch latch ) {
134+ this .latch = latch ;
135+ }
136+
137+ private void processLatch () {
138+ if (latch != null ) {
139+ latch .countDown ();
120140 }
121141 }
122142
123- boolean isExpected () {
124- return isExpected ;
143+ private enum ExpectationState {
144+ AWAITING ,
145+ FINISHING ,
146+ IDLE
125147 }
126- }
148+ }
0 commit comments