66package org .jboss .resteasy .plugins .server .netty ;
77
88import java .io .IOException ;
9+ import java .io .OutputStream ;
910import java .util .concurrent .CompletableFuture ;
1011import java .util .concurrent .CompletionStage ;
1112
1415
1516import io .netty .buffer .ByteBuf ;
1617import io .netty .buffer .Unpooled ;
18+ import io .netty .channel .ChannelFuture ;
1719import io .netty .channel .ChannelHandlerContext ;
1820import io .netty .channel .ChannelPromise ;
1921import io .netty .handler .codec .http .DefaultHttpContent ;
22+ import io .netty .util .concurrent .Future ;
2023
2124/**
2225 * Class to help application that are built to write to an
@@ -43,6 +46,12 @@ public class ChunkOutputStream extends AsyncOutputStream {
4346 private final ByteBuf buffer ;
4447 private final ChannelHandlerContext ctx ;
4548 private final NettyHttpResponse response ;
49+ // All lifecycle state below is guarded by writeLock.
50+ private int pendingWrites = 0 ;
51+ private Throwable writeFailure = null ;
52+ private ChannelPromise responsePromise = null ;
53+ private boolean finishRequested = false ;
54+ private boolean responseWriteStarted = false ;
4655
4756 ChunkOutputStream (final NettyHttpResponse response , final ChannelHandlerContext ctx , final int chunksize ) {
4857 this .response = response ;
@@ -56,6 +65,7 @@ public class ChunkOutputStream extends AsyncOutputStream {
5665 @ Override
5766 public void write (int b ) throws IOException {
5867 synchronized (writeLock ) {
68+ ensureOpen ();
5969 if (buffer .maxWritableBytes () < 1 ) {
6070 flush ();
6171 }
@@ -67,6 +77,8 @@ public void reset() {
6777 if (response .isCommitted ())
6878 throw new IllegalStateException (Messages .MESSAGES .responseIsCommitted ());
6979 synchronized (writeLock ) {
80+ if (finishRequested )
81+ throw new IllegalStateException (Messages .MESSAGES .responseIsCommitted ());
7082 buffer .clear ();
7183 }
7284 }
@@ -88,6 +100,7 @@ private void write(byte[] b, int off, int len, ChannelPromise promise) throws IO
88100 int spaceLeftInCurrentChunk ;
89101 MultiPromise mp = new MultiPromise (ctx , promise );
90102 synchronized (writeLock ) {
103+ ensureOpen ();
91104 while ((spaceLeftInCurrentChunk = buffer .maxWritableBytes ()) < dataLengthLeftToWrite ) {
92105 buffer .writeBytes (b , dataToWriteOffset , spaceLeftInCurrentChunk );
93106 dataToWriteOffset = dataToWriteOffset + spaceLeftInCurrentChunk ;
@@ -109,13 +122,16 @@ public void flush() throws IOException {
109122
110123 private void flush (ChannelPromise promise ) throws IOException {
111124 synchronized (writeLock ) {
125+ ensureOpen ();
112126 int readable = buffer .readableBytes ();
113127 if (readable == 0 ) {
114128 promise .setSuccess ();
115129 return ;
116130 }
117131 if (!response .isCommitted ())
118132 response .prepareChunkStream ();
133+ pendingWrites ++;
134+ promise .addListener (this ::bodyWriteComplete );
119135 ctx .writeAndFlush (new DefaultHttpContent (buffer .copy ()), promise );
120136 buffer .clear ();
121137 }
@@ -157,4 +173,85 @@ public CompletionStage<Void> asyncWrite(byte[] bytes, int offset, int length) {
157173 }
158174 return ret ;
159175 }
176+
177+ /**
178+ * Closes the entity-output lifecycle and completes the HTTP response after every body write has completed.
179+ * The entity stream can wrap this root stream, hence it is flushed while holding the write lock. Any tail emitted by
180+ * that flush is registered before the response is marked as finished.
181+ */
182+ ChannelFuture finish (OutputStream entityOutputStream ) throws IOException {
183+ ChannelPromise result ;
184+ boolean completeResponse ;
185+ synchronized (writeLock ) {
186+ if (finishRequested ) {
187+ return responsePromise ;
188+ }
189+ if (entityOutputStream != null ) {
190+ entityOutputStream .flush ();
191+ }
192+ finishRequested = true ;
193+ responsePromise = ctx .newPromise ();
194+ result = responsePromise ;
195+ completeResponse = pendingWrites == 0 ;
196+ }
197+ if (completeResponse ) {
198+ completeResponse ();
199+ }
200+ return result ;
201+ }
202+
203+ private void ensureOpen () throws IOException {
204+ if (finishRequested ) {
205+ throw new IOException (Messages .MESSAGES .responseIsCommitted ());
206+ }
207+ }
208+
209+ private void bodyWriteComplete (Future <?> future ) {
210+ boolean completeResponse ;
211+ synchronized (writeLock ) {
212+ pendingWrites --;
213+ if (!future .isSuccess () && writeFailure == null ) {
214+ writeFailure = future .cause () == null
215+ ? new IOException ("Response body write failed without a cause" )
216+ : future .cause ();
217+ }
218+ completeResponse = finishRequested && pendingWrites == 0 ;
219+ }
220+ if (!future .isSuccess ()) {
221+ // Once body bytes may have reached the peer, only closing the transport can prevent a clean partial response.
222+ ctx .close ();
223+ }
224+ if (completeResponse ) {
225+ completeResponse ();
226+ }
227+ }
228+
229+ private void completeResponse () {
230+ final ChannelPromise result ;
231+ final Throwable failure ;
232+ synchronized (writeLock ) {
233+ if (!finishRequested || pendingWrites != 0 || responseWriteStarted ) {
234+ return ;
235+ }
236+ responseWriteStarted = true ;
237+ result = responsePromise ;
238+ failure = writeFailure ;
239+ }
240+
241+ if (failure != null ) {
242+ ctx .close ().addListener (ignored -> result .tryFailure (failure ));
243+ return ;
244+ }
245+
246+ response .writeResponseTermination ().addListener (future -> {
247+ if (future .isSuccess ()) {
248+ result .trySuccess ();
249+ } else {
250+ Throwable cause = future .cause () == null
251+ ? new IOException ("Response termination write failed without a cause" )
252+ : future .cause ();
253+ ctx .close ().addListener (ignored -> result .tryFailure (cause ));
254+ }
255+ });
256+ }
160257}
0 commit comments