diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java index 523b596d162..0d51ff0aec2 100644 --- a/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 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,9 +16,6 @@ package io.helidon.microprofile.server; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - import java.util.HashSet; import java.util.Set; @@ -39,9 +36,11 @@ import jakarta.ws.rs.core.UriInfo; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + class JaxRsApplicationPathTest { private static Server server; @@ -87,7 +86,8 @@ void emptyParamTest() { public void testEncoded() { String getResponse = client.target("http://localhost:" + port) .path("/ApplicationPath!/Resource/encoded").queryParam("query", "%dummy23+a") - .request().get(String.class); + .request() + .get(String.class); assertThat(getResponse, is("true:%25dummy23%2Ba")); } diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java index 40328c7ba68..019ec79515d 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -32,6 +32,7 @@ import io.helidon.http.Status; import io.helidon.http.http2.Http2Headers; import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ServerConnectionException; import io.helidon.webserver.http.ServerResponseBase; class Http2ServerResponse extends ServerResponseBase { @@ -74,51 +75,55 @@ public Http2ServerResponse header(Header header) { @Override public void send(byte[] entityBytes) { - if (outputStreamFilter != null) { - // in this case we must honor user's request to filter the stream - try (OutputStream os = outputStream()) { - os.write(entityBytes); - } catch (IOException e) { - throw new UncheckedIOException(e); + try { + if (outputStreamFilter != null) { + // in this case we must honor user's request to filter the stream + try (OutputStream os = outputStream()) { + os.write(entityBytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return; } - return; - } - if (isSent) { - throw new IllegalStateException("Response already sent"); - } - if (streamingEntity) { - throw new IllegalStateException("When output stream is used, response is completed by closing the output stream" - + ", do not call send()."); - } - isSent = true; + if (isSent) { + throw new IllegalStateException("Response already sent"); + } + if (streamingEntity) { + throw new IllegalStateException("When output stream is used, response is completed by closing the output stream" + + ", do not call send()."); + } + isSent = true; - // handle content encoding - byte[] bytes = entityBytes(entityBytes); + // handle content encoding + byte[] bytes = entityBytes(entityBytes); - headers.setIfAbsent(HeaderValues.create(HeaderNames.CONTENT_LENGTH, - true, - false, - String.valueOf(bytes.length))); - headers.setIfAbsent(HeaderValues.create(HeaderNames.DATE, true, false, DateTime.rfc1123String())); + headers.setIfAbsent(HeaderValues.create(HeaderNames.CONTENT_LENGTH, + true, + false, + String.valueOf(bytes.length))); + headers.setIfAbsent(HeaderValues.create(HeaderNames.DATE, true, false, DateTime.rfc1123String())); + + Http2Headers http2Headers = Http2Headers.create(headers); + http2Headers.status(status()); + headers.remove(Http2Headers.STATUS_NAME, it -> ctx.log(LOGGER, + System.Logger.Level.WARNING, + "Status must be configured on response, " + + "do not set HTTP/2 pseudo headers")); - Http2Headers http2Headers = Http2Headers.create(headers); - http2Headers.status(status()); - headers.remove(Http2Headers.STATUS_NAME, it -> ctx.log(LOGGER, - System.Logger.Level.WARNING, - "Status must be configured on response, " - + "do not set HTTP/2 pseudo headers")); + boolean sendTrailers = request.headers().contains(HeaderValues.TE_TRAILERS) || headers.contains(HeaderNames.TRAILER); - boolean sendTrailers = request.headers().contains(HeaderValues.TE_TRAILERS) || headers.contains(HeaderNames.TRAILER); + http2Headers.validateResponse(); + bytesWritten += stream.writeHeadersWithData(http2Headers, bytes.length, BufferData.create(bytes), !sendTrailers); - http2Headers.validateResponse(); - bytesWritten += stream.writeHeadersWithData(http2Headers, bytes.length, BufferData.create(bytes), !sendTrailers); + if (sendTrailers) { + bytesWritten += stream.writeTrailers(Http2Headers.create(trailers)); + } - if (sendTrailers) { - bytesWritten += stream.writeTrailers(Http2Headers.create(trailers)); + afterSend(); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed writing entity", e); } - - afterSend(); } @Override diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java index 92815f049c8..bee1f707da6 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -57,6 +57,7 @@ import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.ConnectionContext; import io.helidon.webserver.Router; +import io.helidon.webserver.ServerConnectionException; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.http2.spi.Http2SubProtocolSelector; import io.helidon.webserver.http2.spi.SubProtocolResult; @@ -195,23 +196,27 @@ public boolean rstStream(Http2RstStream rstStream) { @Override public void windowUpdate(Http2WindowUpdate windowUpdate) { - //5.1/3 - if (state == Http2StreamState.IDLE) { - String msg = "Received WINDOW_UPDATE for stream " + streamId + " in state IDLE"; - Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, msg); - writer.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); - throw new Http2Exception(Http2ErrorCode.PROTOCOL, msg); - } - //6.9/2 - if (windowUpdate.windowSizeIncrement() == 0) { - Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); - } - //6.9.1/3 - long size = flowControl.outbound().incrementStreamWindowSize(windowUpdate.windowSizeIncrement()); - if (size > WindowSize.MAX_WIN_SIZE || size < 0L) { - Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); + try { + //5.1/3 + if (state == Http2StreamState.IDLE) { + String msg = "Received WINDOW_UPDATE for stream " + streamId + " in state IDLE"; + Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, msg); + writer.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); + throw new Http2Exception(Http2ErrorCode.PROTOCOL, msg); + } + //6.9/2 + if (windowUpdate.windowSizeIncrement() == 0) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); + } + //6.9.1/3 + long size = flowControl.outbound().incrementStreamWindowSize(windowUpdate.windowSizeIncrement()); + if (size > WindowSize.MAX_WIN_SIZE || size < 0L) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); + } + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write window update", e); } } @@ -336,7 +341,11 @@ int writeHeaders(Http2Headers http2Headers, boolean endOfStream) { flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS); } - return writer.writeHeaders(http2Headers, streamId, flags, flowControl.outbound()); + try { + return writer.writeHeaders(http2Headers, streamId, flags, flowControl.outbound()); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write headers", e); + } } int writeHeadersWithData(Http2Headers http2Headers, int contentLength, BufferData bufferData, boolean endOfStream) { @@ -353,10 +362,14 @@ int writeHeadersWithData(Http2Headers http2Headers, int contentLength, BufferDat Http2Flag.DataFlags.create(endOfStream ? Http2Flag.END_OF_STREAM : 0), streamId), bufferData); - return writer.writeHeaders(http2Headers, streamId, - Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), - frameData, - flowControl.outbound()); + try { + return writer.writeHeaders(http2Headers, streamId, + Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), + frameData, + flowControl.outbound()); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write headers", e); + } } int writeData(BufferData bufferData, boolean endOfStream) { @@ -373,7 +386,11 @@ int writeData(BufferData bufferData, boolean endOfStream) { streamId), bufferData); - writer.writeData(frameData, flowControl.outbound()); + try { + writer.writeData(frameData, flowControl.outbound()); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write frame data", e); + } return frameData.header().length() + Http2FrameHeader.LENGTH; } @@ -381,10 +398,14 @@ int writeTrailers(Http2Headers http2trailers) { writeState = writeState.checkAndMove(WriteState.TRAILERS_SENT); streams.remove(this.streamId); + try { return writer.writeHeaders(http2trailers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), flowControl.outbound()); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write trailers", e); + } } void write100Continue() { @@ -393,10 +414,14 @@ void write100Continue() { Header status = HeaderValues.createCached(Http2Headers.STATUS_NAME, 100); Http2Headers http2Headers = Http2Headers.create(WritableHeaders.create().add(status)); - writer.writeHeaders(http2Headers, + try { + writer.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), flowControl.outbound()); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write 100-Continue", e); + } } } diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/UncheckedIoExceptionTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/UncheckedIoExceptionTest.java new file mode 100644 index 00000000000..9192aed4796 --- /dev/null +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/UncheckedIoExceptionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 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.webserver.tests; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class UncheckedIoExceptionTest { + @SetUpRoute + static void routing(HttpRules rules) { + rules.get("/fail", ((req, res) -> { + throw new UncheckedIOException("My Exception", new IOException("Outbound client failure")); + })); + } + + @Test + void testUncheckedIoTreatedAsAnyOther(Http1Client client) { + var response = client.get("/fail") + .request(String.class); + + assertThat(response.status(), is(Status.INTERNAL_SERVER_ERROR_500)); + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index 049f6b702ed..ab049a295d1 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; +import java.util.function.Supplier; import javax.net.ssl.SSLSocket; @@ -130,7 +131,7 @@ public final void run() { helidonSocket = PlainSocket.server(socket, channelId, serverChannelId); } - reader = new DataReader(helidonSocket); + reader = new DataReader(new MapExceptionDataSupplier(helidonSocket)); writer = SocketWriter.create(listenerContext.executor(), helidonSocket, listenerContext.config().writeQueueLength()); } catch (Exception e) { @@ -167,12 +168,12 @@ public final void run() { helidonSocket.log(LOGGER, WARNING, "escaped Request exception", e); } catch (HttpException e) { helidonSocket.log(LOGGER, WARNING, "escaped HTTP exception", e); + } catch (ServerConnectionException e) { + // socket exception - the socket failed, probably killed by OS, proxy or client + helidonSocket.log(LOGGER, TRACE, "server I/O issue", e); } catch (CloseConnectionException e) { // end of request stream - safe to close the connection, as it was requested by our client helidonSocket.log(LOGGER, TRACE, "connection close requested", e); - } catch (UncheckedIOException e) { - // socket exception - the socket failed, probably killed by OS, proxy or client - helidonSocket.log(LOGGER, TRACE, "received I/O exception", e); } catch (Exception e) { helidonSocket.log(LOGGER, WARNING, "unexpected exception", e); } finally { @@ -323,4 +324,21 @@ private void closeChannel() { helidonSocket.log(LOGGER, TRACE, "Failed to close socket on connection close", e); } } + + private static class MapExceptionDataSupplier implements Supplier { + private final HelidonSocket helidonSocket; + + private MapExceptionDataSupplier(HelidonSocket helidonSocket) { + this.helidonSocket = helidonSocket; + } + + @Override + public byte[] get() { + try { + return helidonSocket.get(); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to get data from socket", e); + } + } + } } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerConnectionException.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerConnectionException.java new file mode 100644 index 00000000000..afea41fa169 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerConnectionException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 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.webserver; + +import java.util.Objects; + +/** + * An exception that was caused by server communication error (on the original client call). + *

+ * This exception must bubble up through the error handling chain, as otherwise we would treat it + * as an internal exception, and may end up logging too much information. + */ +public class ServerConnectionException extends CloseConnectionException { + /** + * Server connection exception based on a cause. + * + * @param message descriptive message + * @param cause cause of this exception + */ + public ServerConnectionException(String message, Throwable cause) { + super(Objects.requireNonNull(message), + Objects.requireNonNull(cause)); + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/ErrorHandlers.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/ErrorHandlers.java index 0c21e214a17..2b1fcc1fcfc 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http/ErrorHandlers.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/ErrorHandlers.java @@ -16,7 +16,6 @@ package io.helidon.webserver.http; -import java.io.UncheckedIOException; import java.net.SocketException; import java.util.IdentityHashMap; import java.util.Map; @@ -31,6 +30,7 @@ import io.helidon.http.RequestException; import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ServerConnectionException; /** * Http routing Error handlers. @@ -77,7 +77,7 @@ public void runWithErrorHandling(ConnectionContext ctx, if (response.hasEntity()) { response.commit(); } - } catch (CloseConnectionException | UncheckedIOException e) { + } catch (CloseConnectionException e) { // these errors must "bubble up" throw e; } catch (RequestException e) { @@ -119,7 +119,7 @@ public void runWithErrorHandling(ConnectionContext ctx, handleError(ctx, request, response, e); } catch (Throwable e) { if (e.getCause() instanceof SocketException se) { - throw new UncheckedIOException(se); + throw new ServerConnectionException("SocketException during routing", se); } handleError(ctx, request, response, e); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java index e4564d07fba..b5ee32e61b9 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java @@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; @@ -42,6 +41,7 @@ import io.helidon.http.media.MediaContext; import io.helidon.http.media.UnsupportedTypeException; import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ServerConnectionException; /** * Base class for common server response tasks that can be shared across HTTP versions. @@ -214,7 +214,7 @@ protected byte[] entityBytes(byte[] configuredEntity) { os.write(entity); os.close(); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to write response", e); } entity = baos.toByteArray(); encoder.headers(headers()); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java index 6bc1bc08756..ed0c5f0f5ee 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java @@ -50,6 +50,7 @@ import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.ConnectionContext; import io.helidon.webserver.ProxyProtocolData; +import io.helidon.webserver.ServerConnectionException; import io.helidon.webserver.http.DirectTransportRequest; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.http1.spi.Http1Upgrader; @@ -207,7 +208,7 @@ public void handle(Semaphore requestSemaphore) throws InterruptedException { } } - } catch (CloseConnectionException | UncheckedIOException e) { + } catch (CloseConnectionException e) { throw e; } catch (BadRequestException e) { handleRequestException(RequestException.builder() @@ -363,7 +364,11 @@ private void route(HttpPrologue prologue, WritableHeaders headers) { // Expect: 100-continue if (headers.contains(HeaderValues.EXPECT_100)) { if (this.http1Config.continueImmediately()) { - writer.writeNow(BufferData.create(CONTINUE_100)); + try { + writer.writeNow(BufferData.create(CONTINUE_100)); + } catch (UncheckedIOException e) { + throw new ServerConnectionException("Failed to write continue", e); + } } expectContinue = true; } @@ -480,7 +485,11 @@ private void handleRequestException(RequestException e) { sendListener.status(ctx, response.status()); sendListener.headers(ctx, headers); sendListener.data(ctx, buffer); - writer.write(buffer); + try { + writer.write(buffer); + } catch (UncheckedIOException uioe) { + throw new ServerConnectionException("Failed to write request exception", uioe); + } if (response.status() == Status.INTERNAL_SERVER_ERROR_500) { LOGGER.log(WARNING, "Internal server error", e); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java index 80791510f6d..40e6d09b228 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java @@ -19,7 +19,6 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -45,6 +44,7 @@ import io.helidon.http.media.EntityWriter; import io.helidon.http.media.MediaContext; import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ServerConnectionException; import io.helidon.webserver.http.ServerResponseBase; import io.helidon.webserver.http.spi.Sink; import io.helidon.webserver.http.spi.SinkProvider; @@ -186,7 +186,7 @@ public void send(byte[] bytes) { try (OutputStream os = outputStream(skipEncoders)) { os.write(bytes); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to write response", e); } } } @@ -307,7 +307,7 @@ private void handleSinkData(Object data, MediaType mediaType) { } } } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to write sink data", e); } } @@ -544,7 +544,7 @@ void commit() { try { super.close(); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to close server response stream.", e); } } @@ -764,7 +764,7 @@ public void close() { try { bufferedDelegate.close(); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to close server output stream", e); } } @@ -777,7 +777,7 @@ void commit() { flush(); delegate.commit(); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new ServerConnectionException("Failed to flush server output stream", e); } } }