From 2d5738318cf3366f159d2627b1a43d49c025cb35 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Mon, 29 Aug 2022 21:04:08 +0200 Subject: [PATCH] Http exception refactoring (#4804) * Refactor HttpException and RequestException to common HTTP module. Add ErrorHandlers (without actual implementation) to Nima. Use HttpException in Nima where appropriate --- .../FileStorage.java.multipart.mustache | 4 +- .../common/http}/BadRequestException.java | 4 +- .../io/helidon/common/http/DirectHandler.java | 4 - .../common/http/ForbiddenException.java | 42 +++++++ .../helidon/common/http}/HttpException.java | 42 ++++++- .../common/http/InternalServerException.java | 43 +++++++ .../common/http}/NotFoundException.java | 8 +- .../helidon/common/http/RequestException.java | 61 ++-------- docs-internal/http-exceptions.md | 36 ++++++ .../examples/media/multipart/FileStorage.java | 4 +- .../backend/TranslatorBackendService.java | 2 +- .../frontend/TranslatorFrontendService.java | 2 +- .../webserver/examples/basics/Main.java | 2 +- .../webserver/examples/comments/Main.java | 2 +- .../nima/http2/webserver/Http2Stream.java | 94 +++++---------- .../nima/observe/config/ConfigService.java | 10 +- .../observe/health/SingleCheckHandler.java | 10 +- .../nima/observe/info/InfoService.java | 10 +- .../helidon/nima/observe/ObserveSupport.java | 11 +- .../test/resources/logging-test.properties | 6 +- .../staticcontent/ByteRangeRequest.java | 18 +-- .../ClassPathContentHandler.java | 21 +--- .../FileBasedContentHandler.java | 22 ++-- .../staticcontent/StaticContentHandler.java | 54 +++------ .../StaticContentHandlerTest.java | 5 +- .../nima/webserver/ConnectionHandler.java | 4 +- .../nima/webserver/http/DirectHandlers.java | 3 +- ...quest.java => DirectTransportRequest.java} | 28 ++--- .../nima/webserver/http/ErrorHandlers.java | 107 ++++++++++++++++++ .../helidon/nima/webserver/http/Filter.java | 9 ++ .../helidon/nima/webserver/http/Filters.java | 49 +++++--- .../nima/webserver/http/HttpRouting.java | 83 ++++++-------- .../webserver/http/ServerResponseBase.java | 9 +- .../nima/webserver/http1/Http1Connection.java | 68 +++++------ .../nima/webserver/http1/Http1Headers.java | 8 +- .../nima/webserver/http1/Http1Prologue.java | 16 +-- .../webserver/WsUpgradeProvider.java | 8 +- .../webserver/jersey/JerseySupport.java | 2 +- .../ClassPathContentHandler.java | 2 +- .../FileBasedContentHandler.java | 6 +- .../staticcontent/StaticContentHandler.java | 12 +- .../StaticContentHandlerTest.java | 2 +- .../webserver/testsupport/TestClientTest.java | 4 +- .../reactive/webserver/ForwardingHandler.java | 1 + .../reactive/webserver/RequestRouting.java | 2 + .../helidon/reactive/webserver/Response.java | 1 + .../reactive/webserver/ServerRequest.java | 1 + .../reactive/webserver/BytesReuseTest.java | 1 + .../webserver/BytesReuseV2ApiTest.java | 1 + 49 files changed, 531 insertions(+), 413 deletions(-) rename {reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver => common/http/src/main/java/io/helidon/common/http}/BadRequestException.java (94%) create mode 100644 common/http/src/main/java/io/helidon/common/http/ForbiddenException.java rename {reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver => common/http/src/main/java/io/helidon/common/http}/HttpException.java (65%) create mode 100644 common/http/src/main/java/io/helidon/common/http/InternalServerException.java rename {reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver => common/http/src/main/java/io/helidon/common/http}/NotFoundException.java (86%) rename nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java => common/http/src/main/java/io/helidon/common/http/RequestException.java (76%) create mode 100644 docs-internal/http-exceptions.md rename nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/{HttpSimpleRequest.java => DirectTransportRequest.java} (70%) create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ErrorHandlers.java diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileStorage.java.multipart.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileStorage.java.multipart.mustache index 07398574ff8..1dc22c06e02 100644 --- a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileStorage.java.multipart.mustache +++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileStorage.java.multipart.mustache @@ -1,8 +1,8 @@ package {{package}}; -import io.helidon.reactive.webserver.BadRequestException; -import io.helidon.reactive.webserver.NotFoundException; +import io.helidon.common.http.BadRequestException; +import io.helidon.common.http.NotFoundException; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/BadRequestException.java b/common/http/src/main/java/io/helidon/common/http/BadRequestException.java similarity index 94% rename from reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/BadRequestException.java rename to common/http/src/main/java/io/helidon/common/http/BadRequestException.java index a4c6ba6f901..e857612aaff 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/BadRequestException.java +++ b/common/http/src/main/java/io/helidon/common/http/BadRequestException.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package io.helidon.reactive.webserver; - -import io.helidon.common.http.Http; +package io.helidon.common.http; /** * A runtime exception indicating a {@link Http.Status#BAD_REQUEST_400 bad request}. diff --git a/common/http/src/main/java/io/helidon/common/http/DirectHandler.java b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java index cc2f97e762d..3db3c45f375 100644 --- a/common/http/src/main/java/io/helidon/common/http/DirectHandler.java +++ b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java @@ -148,10 +148,6 @@ enum EventType { * Internal server error. */ INTERNAL_ERROR(Http.Status.INTERNAL_SERVER_ERROR_500, true), - /** - * No route was found for the request. - */ - NOT_FOUND(Http.Status.NOT_FOUND_404, true), /** * Other type, please specify expected status code. */ diff --git a/common/http/src/main/java/io/helidon/common/http/ForbiddenException.java b/common/http/src/main/java/io/helidon/common/http/ForbiddenException.java new file mode 100644 index 00000000000..76b615b84ff --- /dev/null +++ b/common/http/src/main/java/io/helidon/common/http/ForbiddenException.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, 2022 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.http; + +/** + * A runtime exception indicating a {@link io.helidon.common.http.Http.Status#FORBIDDEN_403 not found}. + */ +public class ForbiddenException extends HttpException { + + /** + * Creates {@link io.helidon.common.http.ForbiddenException}. + * + * @param message the message + */ + public ForbiddenException(String message) { + super(message, Http.Status.FORBIDDEN_403, null, true); + } + + /** + * Creates {@link io.helidon.common.http.ForbiddenException}. + * + * @param message the message + * @param cause the cause of this exception + */ + public ForbiddenException(String message, Throwable cause) { + super(message, Http.Status.FORBIDDEN_403, cause, true); + } +} diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HttpException.java b/common/http/src/main/java/io/helidon/common/http/HttpException.java similarity index 65% rename from reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HttpException.java rename to common/http/src/main/java/io/helidon/common/http/HttpException.java index 065e1a5bbce..f0580bda340 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/HttpException.java +++ b/common/http/src/main/java/io/helidon/common/http/HttpException.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package io.helidon.reactive.webserver; - -import io.helidon.common.http.Http; +package io.helidon.common.http; /** * Runtime exception for applications. @@ -28,6 +26,7 @@ public class HttpException extends RuntimeException { private final Http.Status status; + private final boolean keepAlive; /** * Creates {@link HttpException} associated with {@link Http.Status#INTERNAL_SERVER_ERROR_500}. @@ -55,11 +54,21 @@ public HttpException(String message, Throwable cause) { * @param status the http status */ public HttpException(String message, Http.Status status) { - super(message); + this(message, status, null); + } - this.status = status; + /** + * Creates {@link HttpException}. + * + * @param message the message + * @param status the http status + * @param keepAlive whether to keep the connection alive + */ + public HttpException(String message, Http.Status status, boolean keepAlive) { + this(message, status, null, keepAlive); } + /** * Creates {@link HttpException}. * @@ -68,9 +77,22 @@ public HttpException(String message, Http.Status status) { * @param cause the cause of this exception */ public HttpException(String message, Http.Status status, Throwable cause) { + this(message, status, cause, false); + } + + /** + * Creates {@link HttpException}. + * + * @param message the message + * @param status the http status + * @param cause the cause of this exception + * @param keepAlive whether to keep this connection alive + */ + public HttpException(String message, Http.Status status, Throwable cause, boolean keepAlive) { super(message, cause); this.status = status; + this.keepAlive = keepAlive; } /** @@ -81,4 +103,14 @@ public HttpException(String message, Http.Status status, Throwable cause) { public final Http.Status status() { return status; } + + /** + * Whether we should attempt to keep the connection alive (if enabled for it). + * Some exceptions may allow the connection to be further used (such as {@link io.helidon.common.http.NotFoundException}. + * + * @return whether to keep alive + */ + public boolean keepAlive() { + return keepAlive; + } } diff --git a/common/http/src/main/java/io/helidon/common/http/InternalServerException.java b/common/http/src/main/java/io/helidon/common/http/InternalServerException.java new file mode 100644 index 00000000000..e5c25b668ce --- /dev/null +++ b/common/http/src/main/java/io/helidon/common/http/InternalServerException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, 2022 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.http; + +/** + * A runtime exception indicating a {@link io.helidon.common.http.Http.Status#INTERNAL_SERVER_ERROR_500 internal server error}. + */ +public class InternalServerException extends HttpException { + /** + * Creates {@link io.helidon.common.http.InternalServerException}. + * + * @param message the message + * @param cause the cause of this exception + */ + public InternalServerException(String message, Throwable cause) { + super(message, Http.Status.INTERNAL_SERVER_ERROR_500, cause); + } + + /** + * Creates {@link io.helidon.common.http.InternalServerException}. + * + * @param message the message + * @param cause the cause of this exception + * @param keepAlive whether to keep the connection alive (if keep alives are enabled) + */ + public InternalServerException(String message, Throwable cause, boolean keepAlive) { + super(message, Http.Status.INTERNAL_SERVER_ERROR_500, cause, keepAlive); + } +} diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/NotFoundException.java b/common/http/src/main/java/io/helidon/common/http/NotFoundException.java similarity index 86% rename from reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/NotFoundException.java rename to common/http/src/main/java/io/helidon/common/http/NotFoundException.java index aba466ef26b..522ed365757 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/NotFoundException.java +++ b/common/http/src/main/java/io/helidon/common/http/NotFoundException.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package io.helidon.reactive.webserver; - -import io.helidon.common.http.Http; +package io.helidon.common.http; /** * A runtime exception indicating a {@link Http.Status#NOT_FOUND_404 not found}. @@ -29,7 +27,7 @@ public class NotFoundException extends HttpException { * @param message the message */ public NotFoundException(String message) { - super(message, Http.Status.NOT_FOUND_404); + super(message, Http.Status.NOT_FOUND_404, null, true); } /** @@ -39,6 +37,6 @@ public NotFoundException(String message) { * @param cause the cause of this exception */ public NotFoundException(String message, Throwable cause) { - super(message, Http.Status.NOT_FOUND_404, cause); + super(message, Http.Status.NOT_FOUND_404, cause, true); } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java b/common/http/src/main/java/io/helidon/common/http/RequestException.java similarity index 76% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java rename to common/http/src/main/java/io/helidon/common/http/RequestException.java index a27e990dd54..9eed8d0ec82 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpException.java +++ b/common/http/src/main/java/io/helidon/common/http/RequestException.java @@ -14,22 +14,17 @@ * limitations under the License. */ -package io.helidon.nima.webserver.http; - -import java.util.Optional; - -import io.helidon.common.http.DirectHandler; -import io.helidon.common.http.HeadersServerResponse; -import io.helidon.common.http.Http; +package io.helidon.common.http; /** - * HTTP exception. This allows custom handlers to be used for different even types. + * Exception that will be handled by {@link io.helidon.common.http.DirectHandler}, unless server request and server response + * are already available, in which case it would be handled by appropriate error handler of routing. + * This exception is not used by clients. */ -public class HttpException extends RuntimeException { +public class RequestException extends RuntimeException { private final DirectHandler.EventType eventType; private final Http.Status status; private final DirectHandler.TransportRequest transportRequest; - private final ServerResponse fullResponse; private final boolean keepAlive; private final HeadersServerResponse responseHeaders; @@ -39,12 +34,11 @@ public class HttpException extends RuntimeException { * * @param builder builder with details to create this instance */ - protected HttpException(Builder builder) { + protected RequestException(Builder builder) { super(builder.message, builder.cause); this.eventType = builder.type; this.status = builder.status; this.transportRequest = builder.request; - this.fullResponse = builder.fullResponse; this.keepAlive = builder.keepAlive; this.responseHeaders = builder.responseHeaders; } @@ -85,16 +79,6 @@ public DirectHandler.TransportRequest request() { return transportRequest; } - /** - * Routing response (if available). This is used to correctly send response through this vehicle to handle - * post-send events. - * - * @return routing response if available - */ - public Optional fullResponse() { - return Optional.ofNullable(fullResponse); - } - /** * Whether to attempt to keep connection alive. * @@ -114,15 +98,14 @@ public HeadersServerResponse responseHeaders() { } /** - * Fluent API builder for {@link io.helidon.nima.webserver.http.HttpException}. + * Fluent API builder for {@link RequestException}. */ - public static class Builder implements io.helidon.common.Builder { + public static class Builder implements io.helidon.common.Builder { private String message; private Throwable cause; private DirectHandler.TransportRequest request; private DirectHandler.EventType type; private Http.Status status; - private ServerResponse fullResponse; private Boolean keepAlive; private final HeadersServerResponse responseHeaders = HeadersServerResponse.create(); @@ -130,7 +113,7 @@ private Builder() { } @Override - public HttpException build() { + public RequestException build() { if (message == null) { message = ""; } @@ -140,7 +123,7 @@ public HttpException build() { if (type == null) { type(DirectHandler.EventType.INTERNAL_ERROR); } - return new HttpException(this); + return new RequestException(this); } /** @@ -165,28 +148,6 @@ public Builder cause(Throwable cause) { return this; } - /** - * Routing request. - * - * @param request request to obtain information from - * @return updated builder - */ - public Builder request(ServerRequest request) { - this.request = HttpSimpleRequest.create(request.prologue(), request.headers()); - return this; - } - - /** - * Routing response to be used to handle response from direct handler. - * - * @param response response to use - * @return updated builder - */ - public Builder response(ServerResponse response) { - this.fullResponse = response; - return this; - } - /** * Transport request with as much information as is available. * @@ -231,7 +192,7 @@ public Builder status(Http.Status status) { * Override default keep alive for this exception. * * @param keepAlive whether to keep connection alive - * @return updated builderw + * @return updated builder */ public Builder setKeepAlive(boolean keepAlive) { this.keepAlive = keepAlive; diff --git a/docs-internal/http-exceptions.md b/docs-internal/http-exceptions.md new file mode 100644 index 00000000000..ccaa292b489 --- /dev/null +++ b/docs-internal/http-exceptions.md @@ -0,0 +1,36 @@ +HTTP Exception Handling +--- + +# WebServer + +The request may fail in three locations: +1. In Helidon server processing code (internal, unexpected error caused by a bug). Such a case will be logged into the server log in + Severe/Error log level and the connection will be terminated. +2. In request processing code, before ServerRequest and ServerResponse are created. Such errors (such as when request cannot be parsed) + are handled by `DirectHandler`, and the server provides as much information as possible to the handler. +3. Exceptions thrown from user routing code, and most server exceptions thrown after `ServerRequest` and `ServerResponse` are created, are handled using error handlers of routing. The exception is `BadRequestException` which is always handled by `DirectHandler` (we may discover lazily that a header does not adhere to specification and thrown this exception) + +The exception class hierarchy that is used (these exceptions should not be caught by user code, unless you want to handle everything on your own): +- `RequestException` - used for case 2 - this is the only exception (final class) used when we need to explicitly throw an exception + to be handled by Helidon server code in a `DirectHandler`. +- `HttpException` - used for case 3 - this is the top level exception class for exceptions that provide a status code and message, and + that can be handled by error handlers. If Error handles do not handle them, they will create a response with the expected error + code, and will send the exception message as entity +- `CloseConnectionException` - this exception can be thrown to terminate current connection +- `UncheckedIOException` - this exception is NOT handled and will cause the connection to be closed (we expect this to be problems such as socket closed etc.) + + +# WebClient + +! we need to discuss this first! + Web client will use `HttpException` and its subclasses (such as `NotFoundException`) in case the entity is read directly +(e.g. the user skips `WebClientResponse`, and uses `as(Class)` to read entity) and the status code returned does not provide +an expected entity (such as any error) + +# HttpException reference +The following HTTP status codes have a specific exception class: + +- 403 - `ForbiddenException` +- 404 - `NotFoundException` +- 400 - `BadRequestException` - ALWAYS handled by `DirectHandler` +- 500 - `InternalServerException` - the error handler will be first attempted for the cause, then for this exception \ No newline at end of file diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java index 80964cb7c76..8825395f665 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java @@ -21,8 +21,8 @@ import java.nio.file.Path; import java.util.stream.Stream; -import io.helidon.reactive.webserver.BadRequestException; -import io.helidon.reactive.webserver.NotFoundException; +import io.helidon.common.http.BadRequestException; +import io.helidon.common.http.NotFoundException; /** * Simple bean to managed a directory based storage. diff --git a/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java index 035abe75bcb..2786761bb11 100644 --- a/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java +++ b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java @@ -18,7 +18,7 @@ import java.util.HashMap; import java.util.Map; -import io.helidon.reactive.webserver.BadRequestException; +import io.helidon.common.http.BadRequestException; import io.helidon.reactive.webserver.Routing; import io.helidon.reactive.webserver.ServerRequest; import io.helidon.reactive.webserver.ServerResponse; diff --git a/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java index 9bd7f4ce151..acbc07a40c8 100644 --- a/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java +++ b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java @@ -18,7 +18,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import io.helidon.reactive.webserver.BadRequestException; +import io.helidon.common.http.BadRequestException; import io.helidon.reactive.webserver.Routing; import io.helidon.reactive.webserver.ServerRequest; import io.helidon.reactive.webserver.ServerResponse; diff --git a/examples/webserver/basics/src/main/java/io/helidon/reactive/webserver/examples/basics/Main.java b/examples/webserver/basics/src/main/java/io/helidon/reactive/webserver/examples/basics/Main.java index a8d7558ed36..1a353191763 100644 --- a/examples/webserver/basics/src/main/java/io/helidon/reactive/webserver/examples/basics/Main.java +++ b/examples/webserver/basics/src/main/java/io/helidon/reactive/webserver/examples/basics/Main.java @@ -23,13 +23,13 @@ import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpMediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.reactive.media.common.MediaContext; import io.helidon.reactive.media.common.MessageBodyReader; import io.helidon.reactive.media.jsonp.JsonpSupport; import io.helidon.reactive.webserver.Handler; -import io.helidon.reactive.webserver.HttpException; import io.helidon.reactive.webserver.RequestPredicate; import io.helidon.reactive.webserver.Routing; import io.helidon.reactive.webserver.WebServer; diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/reactive/webserver/examples/comments/Main.java b/examples/webserver/comment-aas/src/main/java/io/helidon/reactive/webserver/examples/comments/Main.java index 290607c23ab..7cd9c0bbc0e 100644 --- a/examples/webserver/comment-aas/src/main/java/io/helidon/reactive/webserver/examples/comments/Main.java +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/reactive/webserver/examples/comments/Main.java @@ -20,8 +20,8 @@ import java.util.concurrent.CompletionException; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.config.Config; -import io.helidon.reactive.webserver.HttpException; import io.helidon.reactive.webserver.Routing; import io.helidon.reactive.webserver.WebServer; diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index 25506492cae..6595a7a9b67 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -18,7 +18,6 @@ import java.io.UncheckedIOException; import java.util.List; -import java.util.Optional; import java.util.ServiceLoader; import java.util.concurrent.ArrayBlockingQueue; @@ -30,6 +29,7 @@ import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http.HeaderValue; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.RequestException; import io.helidon.common.socket.SocketWriterException; import io.helidon.nima.http.encoding.ContentDecoder; import io.helidon.nima.http2.FlowControl; @@ -48,11 +48,10 @@ import io.helidon.nima.http2.Http2WindowUpdate; import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; import io.helidon.nima.http2.webserver.spi.SubProtocolResult; +import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRouting; -import io.helidon.nima.webserver.http.ServerResponse; /** * Server HTTP/2 stream implementation. @@ -262,21 +261,12 @@ public void run() { .setName("[" + ctx.socketId() + " " + ctx.childSocketId() + " ] - " + streamId); try { - try { - handle(); - } catch (UncheckedIOException | SocketWriterException e) { - // failed to write to socket, this happens (remote connection closed, network issues) - ctx.log(LOGGER, System.Logger.Level.TRACE, "Failed to write to socket", e); - } catch (HttpException e) { - throw e; - } catch (Throwable e) { - throw HttpException.builder() - .message("Internal error") - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(e) - .build(); - } - } catch (HttpException e) { + handle(); + } catch (SocketWriterException e) { + throw new CloseConnectionException(e.getMessage(), e); + } catch (CloseConnectionException | UncheckedIOException e) { + throw e; + } catch (RequestException e) { DirectHandler handler = ctx.directHandlers().handler(e.eventType()); DirectHandler.TransportResponse response = handler.handle(e.request(), e.eventType(), @@ -284,37 +274,27 @@ public void run() { e.responseHeaders(), e); - Optional fullResponse = e.fullResponse(); - if (fullResponse.isPresent()) { - fullResponse.ifPresent(res -> { - res.status(response.status()); - response.headers() - .forEach(res::header); - response.entity().ifPresentOrElse(res::send, res::send); - }); + HeadersServerResponse headers = response.headers(); + byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES); + if (message.length != 0) { + headers.set(HeaderValue.create(Header.CONTENT_LENGTH, String.valueOf(message.length))); + } + Http2Headers http2Headers = Http2Headers.create(headers); + if (message.length == 0) { + writer.writeHeaders(http2Headers, + streamId, + Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), + flowControl); } else { - HeadersServerResponse headers = response.headers(); - byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES); - if (message.length != 0) { - headers.set(HeaderValue.create(Header.CONTENT_LENGTH, String.valueOf(message.length))); - } - Http2Headers http2Headers = Http2Headers.create(headers); - if (message.length == 0) { - writer.writeHeaders(http2Headers, - streamId, - Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), - flowControl); - } else { - Http2FrameHeader dataHeader = Http2FrameHeader.create(message.length, - Http2FrameTypes.DATA, - Http2Flag.DataFlags.create(Http2Flag.END_OF_STREAM), - streamId); - writer.writeHeaders(http2Headers, - streamId, - Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), - new Http2FrameData(dataHeader, BufferData.create(message)), - flowControl); - } + Http2FrameHeader dataHeader = Http2FrameHeader.create(message.length, + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(Http2Flag.END_OF_STREAM), + streamId); + writer.writeHeaders(http2Headers, + streamId, + Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), + new Http2FrameData(dataHeader, BufferData.create(message)), + flowControl); } } finally { headers = null; @@ -394,26 +374,8 @@ private void handle() { streamId, this::readEntityFromPipeline); Http2ServerResponse response = new Http2ServerResponse(ctx, request, writer, streamId, flowControl); - try { routing.route(ctx, request, response); - } catch (HttpException e) { - throw HttpException.builder() - .request(request) - .response(response) - .type(e.eventType()) - .status(e.status()) - .message(e.getMessage()) - .cause(e) - .build(); - } catch (Throwable e) { - throw HttpException.builder() - .request(request) - .response(response) - .type(DirectHandler.EventType.INTERNAL_ERROR) - .message(e.getMessage()) - .cause(e) - .build(); } finally { this.state = Http2StreamState.CLOSED; } diff --git a/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java b/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java index a43aea0723d..e0a318865a0 100644 --- a/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java +++ b/nima/observe/config/src/main/java/io/helidon/nima/observe/config/ConfigService.java @@ -23,13 +23,12 @@ import java.util.Set; import java.util.regex.Pattern; -import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.NotFoundException; import io.helidon.config.Config; import io.helidon.config.ConfigValue; import io.helidon.nima.Nima; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRules; import io.helidon.nima.webserver.http.HttpService; import io.helidon.nima.webserver.http.ServerRequest; @@ -101,12 +100,7 @@ private void value(ServerRequest req, ServerResponse res) { json.add("value", obfuscate(name, value.get())); write(req, res, json.build()); } else { - throw HttpException.builder() - .type(DirectHandler.EventType.NOT_FOUND) - .message("Config value for key: " + name) - .request(req) - .response(res) - .build(); + throw new NotFoundException("Config value for key: " + name); } } diff --git a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java index e781d410ea8..abe6a3f4641 100644 --- a/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java +++ b/nima/observe/health/src/main/java/io/helidon/nima/observe/health/SingleCheckHandler.java @@ -21,15 +21,14 @@ import java.util.HashMap; import java.util.Map; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; +import io.helidon.common.http.NotFoundException; import io.helidon.health.HealthCheck; import io.helidon.health.HealthCheckResponse; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; import io.helidon.nima.webserver.http.Handler; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; @@ -54,12 +53,7 @@ public void handle(ServerRequest req, ServerResponse res) { String name = req.path().pathParameters().value("name"); HealthCheck check = checks.get(name); if (check == null) { - throw HttpException.builder() - .request(req) - .response(res) - .type(DirectHandler.EventType.NOT_FOUND) - .message("Health check " + name + " does not exist") - .build(); + throw new NotFoundException(name); } HealthCheckResponse response; diff --git a/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java b/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java index 244bda57912..c25b8e03da6 100644 --- a/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java +++ b/nima/observe/info/src/main/java/io/helidon/nima/observe/info/InfoService.java @@ -19,11 +19,10 @@ import java.util.LinkedHashMap; import java.util.Map; -import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.NotFoundException; import io.helidon.config.Config; import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRules; import io.helidon.nima.webserver.http.HttpService; import io.helidon.nima.webserver.http.ServerRequest; @@ -61,12 +60,7 @@ private void namedInfo(ServerRequest req, ServerResponse res) { Object value = info.get(name); if (value == null) { - throw HttpException.builder() - .type(DirectHandler.EventType.NOT_FOUND) - .message("Application info value for " + name + " is not defined.") - .request(req) - .response(res) - .build(); + throw new NotFoundException("Application info value for " + name + " is not defined."); } else { JsonObjectBuilder json = JSON.createObjectBuilder() .add("name", name) diff --git a/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java b/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java index a6895ef16f7..d618afb33e7 100644 --- a/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java +++ b/nima/observe/observe/src/main/java/io/helidon/nima/observe/ObserveSupport.java @@ -22,13 +22,12 @@ import java.util.function.Consumer; import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.config.Config; import io.helidon.nima.Nima; import io.helidon.nima.observe.spi.ObserveProvider; import io.helidon.nima.webserver.cors.CorsSupport; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRouting; /** @@ -91,13 +90,7 @@ public void accept(HttpRouting.Builder builder) { } } else { builder.get(endpoint, (req, res) -> { - throw HttpException.builder() - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.SERVICE_UNAVAILABLE_503) - .message("Observe endpoint is disabled") - .request(req) - .response(res) - .build(); + throw new HttpException("Observe endpoint is disabled", Http.Status.SERVICE_UNAVAILABLE_503, true); }); } } diff --git a/nima/tests/integration/webserver/webserver/src/test/resources/logging-test.properties b/nima/tests/integration/webserver/webserver/src/test/resources/logging-test.properties index 0fe286be554..cbd14c1bc5f 100644 --- a/nima/tests/integration/webserver/webserver/src/test/resources/logging-test.properties +++ b/nima/tests/integration/webserver/webserver/src/test/resources/logging-test.properties @@ -19,7 +19,7 @@ java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %4$s %3$s %5$s%6$s%n # Global logging level. Can be overridden by specific loggers .level=INFO -io.helidon.nima.level=WARNING -io.helidon.nima.webserver.http.Http1Connection.level=SEVERE -io.helidon.nima.webserver.http.HttpRouting.level=SEVERE +io.helidon.nima.level=FINEST +io.helidon.nima.webserver.http.Http1Connection.level=FINEST +io.helidon.nima.webserver.http.HttpRouting.level=FINEST diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java index 649fc0c00a4..ecbb5e94577 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ByteRangeRequest.java @@ -21,10 +21,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.BadRequestException; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; -import io.helidon.nima.webserver.http.HttpException; +import io.helidon.common.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; @@ -62,12 +62,7 @@ static List parse(ServerRequest req, ServerResponse res, Strin parts.add(ByteRangeRequest.create(req, res, from, last, fileLength)); } if (!found) { - throw HttpException.builder() - .request(req) - .response(res) - .type(DirectHandler.EventType.BAD_REQUEST) - .message("Invalid range header") - .build(); + throw new BadRequestException("Invalid range header"); } return parts; @@ -88,12 +83,7 @@ void setContentRange(ServerResponse response) { private static ByteRangeRequest create(ServerRequest req, ServerResponse res, long offset, long last, long fileLength) { if (offset >= fileLength || last < offset) { res.header(Header.CONTENT_RANGE.withValue("*/" + fileLength)); - throw HttpException.builder() - .request(req) - .response(res) - .type(DirectHandler.EventType.BAD_REQUEST) - .status(Http.Status.REQUESTED_RANGE_NOT_SATISFIABLE_416) - .build(); + throw new HttpException("Wrong range", Http.Status.REQUESTED_RANGE_NOT_SATISFIABLE_416, true); } long length = (last - offset) + 1; diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java index f2902c6b4d4..e013873d061 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/ClassPathContentHandler.java @@ -36,9 +36,8 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; -import io.helidon.nima.webserver.http.HttpException; +import io.helidon.common.http.InternalServerException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; @@ -69,11 +68,7 @@ class ClassPathContentHandler extends FileBasedContentHandler { try { return Files.createTempFile(prefix, suffix); } catch (IOException e) { - throw HttpException.builder() - .message("Static content processing issue") - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(e) - .build(); + throw new InternalServerException("Failed to create temporary file", e, true); } }; } else { @@ -81,11 +76,7 @@ class ClassPathContentHandler extends FileBasedContentHandler { try { return Files.createTempFile(tmpDir, prefix, suffix); } catch (IOException e) { - throw HttpException.builder() - .message("Static content processing issue") - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(e) - .build(); + throw new InternalServerException("Failed to create temporary file", e, true); } }; } @@ -265,11 +256,7 @@ private ExtractedJarEntry extractJarEntry(URL url) { } } } catch (IOException ioe) { - throw HttpException.builder() - .message("Cannot load JAR file!") - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(ioe) - .build(); + throw new InternalServerException("Cannot load resource", ioe); } } diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java index aa3a2e21346..d4434f58204 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/FileBasedContentHandler.java @@ -32,16 +32,16 @@ import java.util.Objects; import java.util.Optional; -import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.ForbiddenException; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http.HeaderValues; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpMediaType; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; @@ -120,11 +120,7 @@ void sendFile(Http.Method method, // now it exists and is a file if (!Files.isRegularFile(path) || !Files.isReadable(path) || Files.isHidden(path)) { - throw HttpException.builder() - .message("File is not accessible") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.FORBIDDEN_403) - .build(); + throw new ForbiddenException("File is not accessible"); } // Caching headers support @@ -206,12 +202,12 @@ void send(ServerRequest request, ServerResponse response, Path path) throws IOEx } /** - * Find welcome file in provided directory or throw not found {@link io.helidon.nima.webserver.http.HttpException}. + * Find welcome file in provided directory or throw not found {@link io.helidon.common.http.RequestException}. * * @param directory a directory to find in * @param name welcome file name * @return a path of the welcome file - * @throws io.helidon.nima.webserver.http.HttpException if welcome file doesn't exists + * @throws io.helidon.common.http.RequestException if welcome file doesn't exists */ private static Path resolveWelcomeFile(Path directory, String name) { throwNotFoundIf(name == null || name.isEmpty()); @@ -233,11 +229,9 @@ private MediaType detectType(String fileName, HeadersServerRequest requestHeader if (requestHeaders.isAccepted(it)) { return it; } - throw HttpException.builder() - .message("Media type " + it + " is not accepted by request") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.UNSUPPORTED_MEDIA_TYPE_415) - .build(); + throw new HttpException("Media type " + it + " is not accepted by request", + Http.Status.UNSUPPORTED_MEDIA_TYPE_415, + true); }) .orElseGet(() -> { List acceptedTypes = requestHeaders.acceptedTypes(); diff --git a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java index 82d7dfc8db8..5cc68e639d3 100644 --- a/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java +++ b/nima/webserver/static-content/src/main/java/io/helidon/nima/webserver/staticcontent/StaticContentHandler.java @@ -25,13 +25,15 @@ import java.util.Optional; import java.util.function.Function; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; +import io.helidon.common.http.HttpException; +import io.helidon.common.http.InternalServerException; +import io.helidon.common.http.NotFoundException; +import io.helidon.common.http.RequestException; import io.helidon.common.uri.UriQuery; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.HttpRules; import io.helidon.nima.webserver.http.PathMatchers; import io.helidon.nima.webserver.http.ServerRequest; @@ -59,7 +61,7 @@ abstract class StaticContentHandler implements StaticContentSupport { * @param etag the proposed ETag. If {@code null} then method returns false * @param requestHeaders an HTTP request headers * @param responseHeaders an HTTP response headers - * @throws io.helidon.nima.webserver.http.HttpException if ETag is checked + * @throws io.helidon.common.http.RequestException if ETag is checked */ static void processEtag(String etag, HeadersServerRequest requestHeaders, HeadersServerResponse responseHeaders) { if (etag == null || etag.isEmpty()) { @@ -74,11 +76,8 @@ static void processEtag(String etag, HeadersServerRequest requestHeaders, Header for (String ifNoneMatch : ifNoneMatches) { ifNoneMatch = unquoteETag(ifNoneMatch); if ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)) { - throw HttpException.builder() - .message("Accepted by If-None-Match header!") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.NOT_MODIFIED_304) - .build(); + // using exception to handle normal flow (same as in reactive static content) + throw new HttpException("Accepted by If-None-Match header", Http.Status.NOT_MODIFIED_304, true); } } } @@ -96,11 +95,7 @@ static void processEtag(String etag, HeadersServerRequest requestHeaders, Header } } if (!ifMatchChecked) { - throw HttpException.builder() - .message("Not accepted by If-Match header!") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.PRECONDITION_FAILED_412) - .build(); + throw new HttpException("Not accepted by If-Match header", Http.Status.PRECONDITION_FAILED_412, true); } } } @@ -113,7 +108,7 @@ static void processEtag(String etag, HeadersServerRequest requestHeaders, Header * @param modified the last modification instance. If {@code null} then method just returns {@code false}. * @param requestHeaders an HTTP request headers * @param responseHeaders an HTTP response headers - * @throws io.helidon.nima.webserver.http.HttpException if (un)modify since header is checked + * @throws io.helidon.common.http.RequestException if (un)modify since header is checked */ static void processModifyHeaders(Instant modified, HeadersServerRequest requestHeaders, @@ -128,37 +123,26 @@ static void processModifyHeaders(Instant modified, .ifModifiedSince() .map(ChronoZonedDateTime::toInstant); if (ifModSince.isPresent() && !ifModSince.get().isBefore(modified)) { - throw HttpException.builder() - .message("Not valid for If-Modified-Since header!") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.NOT_MODIFIED_304) - .build(); + throw new HttpException("Not valid for If-Modified-Since header", Http.Status.NOT_MODIFIED_304, true); } // If-Unmodified-Since Optional ifUnmodSince = requestHeaders .ifUnmodifiedSince() .map(ChronoZonedDateTime::toInstant); if (ifUnmodSince.isPresent() && ifUnmodSince.get().isBefore(modified)) { - throw HttpException.builder() - .message("Not valid for If-Unmodified-Since header!") - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.PRECONDITION_FAILED_412) - .build(); + throw new HttpException("Not valid for If-Unmodified-Since header", Http.Status.PRECONDITION_FAILED_412, true); } } /** - * If provided {@code condition} is {@code true} then throws not found {@link io.helidon.nima.webserver.http.HttpException}. + * If provided {@code condition} is {@code true} then throws not found {@link io.helidon.common.http.RequestException}. * * @param condition if condition is true then throws an exception otherwise not - * @throws io.helidon.nima.webserver.http.HttpException if {@code condition} parameter is {@code true}. + * @throws io.helidon.common.http.RequestException if {@code condition} parameter is {@code true}. */ static void throwNotFoundIf(boolean condition) { if (condition) { - throw HttpException.builder() - .message("Static content not found!") - .type(DirectHandler.EventType.NOT_FOUND) - .build(); + throw new NotFoundException("Static content not found"); } } @@ -231,7 +215,7 @@ void handle(ServerRequest request, ServerResponse response) { if (!doHandle(method, requestPath, request, response)) { response.next(); } - } catch (HttpException httpException) { + } catch (RequestException httpException) { if (httpException.status().code() == Http.Status.NOT_FOUND_404.code()) { // Prefer to next() before NOT_FOUND response.next(); @@ -240,11 +224,7 @@ void handle(ServerRequest request, ServerResponse response) { } } catch (Exception e) { LOGGER.log(Level.TRACE, "Failed to access static resource", e); - throw HttpException.builder() - .message("Cannot access static resource!") - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(e) - .build(); + throw new InternalServerException("Cannot access static resource", e); } } @@ -257,7 +237,7 @@ void handle(ServerRequest request, ServerResponse response) { * @param response an HTTP response * @return {@code true} only if static content was found and processed. * @throws java.io.IOException if resource is not acceptable - * @throws io.helidon.nima.webserver.http.HttpException if some known WEB error + * @throws io.helidon.common.http.RequestException if some known WEB error */ abstract boolean doHandle(Http.Method method, String requestedPath, ServerRequest request, ServerResponse response) throws IOException, URISyntaxException; diff --git a/nima/webserver/static-content/src/test/java/io/helidon/nima/webserver/staticcontent/StaticContentHandlerTest.java b/nima/webserver/static-content/src/test/java/io/helidon/nima/webserver/staticcontent/StaticContentHandlerTest.java index e47722a3d61..bb26666378e 100644 --- a/nima/webserver/static-content/src/test/java/io/helidon/nima/webserver/staticcontent/StaticContentHandlerTest.java +++ b/nima/webserver/static-content/src/test/java/io/helidon/nima/webserver/staticcontent/StaticContentHandlerTest.java @@ -27,12 +27,13 @@ import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.HeadersServerResponse; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.RequestException; import io.helidon.common.parameters.Parameters; import io.helidon.common.uri.UriFragment; import io.helidon.common.uri.UriPath; import io.helidon.common.uri.UriQuery; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http.RoutedPath; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; @@ -210,7 +211,7 @@ private static void assertHttpException(Runnable runnable, Http.Status status) { throw new AssertionError("Expected HttpException was not thrown!"); } catch (HttpException he) { if (status != null && status.code() != he.status().code()) { - throw new AssertionError("Unexpected status in HttpException. " + throw new AssertionError("Unexpected status in RequestException. " + "(Expected: " + status.code() + ", Actual: " + status.code() + ")"); } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java index cddd1578dc2..c122faa12f0 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java @@ -23,12 +23,12 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; +import io.helidon.common.http.RequestException; import io.helidon.common.socket.HelidonSocket; import io.helidon.common.socket.SocketWriter; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webserver.http.DirectHandlers; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.spi.ServerConnection; import io.helidon.nima.webserver.spi.ServerConnectionProvider; @@ -121,7 +121,7 @@ public final void run() { } finally { writer.close(); } - } catch (HttpException e) { + } catch (RequestException e) { ctx.log(LOGGER, WARNING, "escaped HTTP exception", e); } catch (CloseConnectionException e) { // end of request stream - safe to close the connection, as it was requested by our client diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java index 31f74793869..784fbd39829 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectHandlers.java @@ -22,6 +22,7 @@ import io.helidon.common.http.DirectHandler; import io.helidon.common.http.DirectHandler.EventType; import io.helidon.common.http.Http; +import io.helidon.common.http.RequestException; import io.helidon.nima.webserver.CloseConnectionException; import static java.lang.System.Logger.Level.WARNING; @@ -64,7 +65,7 @@ public DirectHandler handler(EventType eventType) { * @param httpException exception to handle * @param res response */ - public void handle(HttpException httpException, ServerResponse res) { + public void handle(RequestException httpException, ServerResponse res) { DirectHandler.TransportResponse response = handler(httpException.eventType()).handle( httpException.request(), httpException.eventType(), diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectTransportRequest.java similarity index 70% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java rename to nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectTransportRequest.java index 3bb70391fa6..4e95b44fbbb 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpSimpleRequest.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/DirectTransportRequest.java @@ -23,18 +23,18 @@ import io.helidon.common.http.HttpPrologue; /** - * Simple request to use with {@link HttpException}. + * Simple request to use with {@link io.helidon.common.http.RequestException}. */ -public class HttpSimpleRequest implements DirectHandler.TransportRequest { +public class DirectTransportRequest implements DirectHandler.TransportRequest { private final String version; private final String method; private final String path; private final HeadersServerRequest headers; - private HttpSimpleRequest(String version, - String method, - String path, - HeadersServerRequest headers) { + private DirectTransportRequest(String version, + String method, + String path, + HeadersServerRequest headers) { this.version = version; this.method = method; this.path = path; @@ -52,10 +52,10 @@ private HttpSimpleRequest(String version, public static DirectHandler.TransportRequest create(String protocolAndVersion, String method, String path) { - return new HttpSimpleRequest(protocolAndVersion, - method, - path, - HeadersServerRequest.create(HeadersWritable.create())); + return new DirectTransportRequest(protocolAndVersion, + method, + path, + HeadersServerRequest.create(HeadersWritable.create())); } /** @@ -66,10 +66,10 @@ public static DirectHandler.TransportRequest create(String protocolAndVersion, * @return a new simple request */ public static DirectHandler.TransportRequest create(HttpPrologue prologue, Headers headers) { - return new HttpSimpleRequest(prologue.protocol() + "/" + prologue.protocolVersion(), - prologue.method().text(), - prologue.uriPath().rawPathNoParams(), - HeadersServerRequest.create(headers)); + return new DirectTransportRequest(prologue.protocol() + "/" + prologue.protocolVersion(), + prologue.method().text(), + prologue.uriPath().rawPathNoParams(), + HeadersServerRequest.create(headers)); } @Override diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ErrorHandlers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ErrorHandlers.java new file mode 100644 index 00000000000..a485e68348a --- /dev/null +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ErrorHandlers.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 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.webserver.http; + +import java.io.UncheckedIOException; + +import io.helidon.common.http.BadRequestException; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.HttpException; +import io.helidon.common.http.InternalServerException; +import io.helidon.common.http.RequestException; +import io.helidon.nima.webserver.CloseConnectionException; +import io.helidon.nima.webserver.ConnectionContext; + +/** + * Http routing Error handlers. + */ +public class ErrorHandlers { + ErrorHandlers() { + } + + /** + * Run a task and handle the errors, if any. Uses error handlers configured on routing. + * Correctly handles all expected (and unexpected) exceptions that can happen in filters and routes. + * + * @param ctx connection context + * @param request HTTP server request + * @param response HTTP server response + * @param task task to execute + */ + public void runWithErrorHandling(ConnectionContext ctx, ServerRequest request, ServerResponse response, Runnable task) { + try { + task.run(); + } catch (CloseConnectionException | UncheckedIOException e) { + // these errors must "bubble up" + throw e; + } catch (RequestException e) { + handleRequestException(ctx, response, e); + } catch (BadRequestException e) { + // bad request exception MUST be handled by direct handlers + handleRequestException(ctx, response, RequestException.builder() + .message(e.getMessage()) + .cause(e) + .type(DirectHandler.EventType.BAD_REQUEST) + .status(e.status()) + .setKeepAlive(e.keepAlive()) + .build()); + } catch (InternalServerException e) { + // this is the place error handling must be done + // check if error handler exists for cause - if so, use it + if (hasErrorHandler(e.getCause())) { + handleError(ctx, request, response, e.getCause()); + } else { + handleError(ctx, request, response, e); + } + } catch (HttpException e) { + handleError(ctx, request, response, e); + } catch (RuntimeException e) { + handleError(ctx, request, response, e); + } + } + + private void handleRequestException(ConnectionContext ctx, ServerResponse response, RequestException e) { + ctx.directHandlers().handle(e, response); + } + + private boolean hasErrorHandler(Throwable cause) { + // TODO needs implementation (separate issue) + return true; + } + + private void handleError(ConnectionContext ctx, ServerRequest request, ServerResponse response, Throwable e) { + // to be handled by error handler + handleRequestException(ctx, response, RequestException.builder() + .cause(e) + .type(DirectHandler.EventType.INTERNAL_ERROR) + .message(e.getMessage()) + .request(DirectTransportRequest.create(request.prologue(), request.headers())) + .build()); + } + + private void handleError(ConnectionContext ctx, ServerRequest request, ServerResponse response, HttpException e) { + // to be handled by error handler + handleRequestException(ctx, response, RequestException.builder() + .cause(e) + .type(DirectHandler.EventType.OTHER) + .message(e.getMessage()) + .status(e.status()) + .setKeepAlive(e.keepAlive()) + .request(DirectTransportRequest.create(request.prologue(), request.headers())) + .build()); + } +} diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filter.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filter.java index d6a0ce332ec..d7f73e28d8c 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filter.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filter.java @@ -25,6 +25,15 @@ public interface Filter { * If request processing should be terminated, do not call {@link FilterChain#proceed()}. * If request processing should continue with next filter, * call {@link FilterChain#proceed()}. + * {@link io.helidon.nima.webserver.http.FilterChain#proceed()} may throw an exception. The following exceptions + * mean the connection will close (and the request cannot finish): + *
    + *
  • {@link io.helidon.nima.webserver.CloseConnectionException}
  • + *
  • {@link java.io.UncheckedIOException}
  • + *
+ * Filters may throw {@link io.helidon.common.http.HttpException} (or its subclasses), all filters before the current + * filter will not receive such an exception - it will be processed before the method + * {@link io.helidon.nima.webserver.http.FilterChain#proceed()} returns. * * @param chain to proceed with further filters (or routing if this is the last filter) * @param req routing request diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java index 27200a1065d..bf10dd52012 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/Filters.java @@ -19,18 +19,22 @@ import java.util.Iterator; import java.util.List; -import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.parameters.Parameters; import io.helidon.common.uri.UriPath; +import io.helidon.nima.webserver.ConnectionContext; /** * Handler of HTTP filters. */ public class Filters { + private final ErrorHandlers errorHandlers; private final List filters; private final boolean noFilters; - private Filters(List filters) { + private Filters(ErrorHandlers errorHandlers, List filters) { + this.errorHandlers = errorHandlers; this.filters = filters; this.noFilters = filters.isEmpty(); } @@ -38,42 +42,50 @@ private Filters(List filters) { /** * Create filters. * - * @param filters list of filters to use + * @param errorHandlers + * @param filters list of filters to use * @return filters */ - public static Filters create(List filters) { - return new Filters(filters); + public static Filters create(ErrorHandlers errorHandlers, List filters) { + return new Filters(errorHandlers, filters); } /** * Filter request. * + * @param ctx connection context * @param request request * @param response response * @param routingExecutor this handler is called after all filters finish processing * (unless a filter does not invoke the chain) */ - public void filter(RoutingRequest request, RoutingResponse response, Runnable routingExecutor) { + public void filter(ConnectionContext ctx, RoutingRequest request, RoutingResponse response, Runnable routingExecutor) { if (noFilters) { - routingExecutor.run(); + errorHandlers.runWithErrorHandling(ctx, request, response, routingExecutor); return; } - FilterChain chain = new FilterChainImpl(filters, request, response, routingExecutor); + FilterChain chain = new FilterChainImpl(ctx, errorHandlers, filters, request, response, routingExecutor); request.path(new FilterRoutedPath(request.prologue().uriPath())); chain.proceed(); } private static final class FilterChainImpl implements FilterChain { + private final ConnectionContext ctx; + private final ErrorHandlers errorHandlers; private final Iterator filters; private final Runnable routingExecutor; private RoutingRequest request; private RoutingResponse response; - private FilterChainImpl(List filters, + private FilterChainImpl(ConnectionContext ctx, + ErrorHandlers errorHandlers, + List filters, RoutingRequest request, RoutingResponse response, Runnable routingExecutor) { + this.ctx = ctx; + this.errorHandlers = errorHandlers; this.filters = filters.iterator(); this.request = request; this.response = response; @@ -86,20 +98,21 @@ public void proceed() { return; } if (filters.hasNext()) { - filters.next().filter(this, request, response); + errorHandlers.runWithErrorHandling(ctx, request, response, this::runNextFilter); } else { - routingExecutor.run(); + errorHandlers.runWithErrorHandling(ctx, request, response, routingExecutor); if (!response.isSent()) { - throw HttpException.builder() - .request(request) - .response(response) - .type(DirectHandler.EventType.INTERNAL_ERROR) - .message("Routing finished but response was not sent. Níma does not support asynchronous responses. " - + "Please block until a response is sent.") - .build(); + // intentionally not using InternalServerException, as we do not have a cause + throw new HttpException("Routing finished but response was not sent. Níma does not support asynchronous" + + " responses. Please block until a response can be sent.", + Http.Status.INTERNAL_SERVER_ERROR_500); } } } + + private void runNextFilter() { + filters.next().filter(this, request, response); + } } private static final class FilterRoutedPath implements RoutedPath { diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java index 69a2a8f0dbc..a8af327c137 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/HttpRouting.java @@ -25,9 +25,12 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.InternalServerException; +import io.helidon.common.http.NotFoundException; +import io.helidon.common.http.RequestException; import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Routing; @@ -42,9 +45,11 @@ public final class HttpRouting implements Routing { private final Filters filters; private final ServiceRoute rootRoute; + // todo configure on HTTP routing + private final ErrorHandlers errorHandlers = new ErrorHandlers(); private HttpRouting(Builder builder) { - this.filters = Filters.create(List.copyOf(builder.filters)); + this.filters = Filters.create(errorHandlers, List.copyOf(builder.filters)); this.rootRoute = builder.rootRules.build(); } @@ -88,8 +93,10 @@ public static HttpRouting empty() { * @param response routing response */ public void route(ConnectionContext ctx, RoutingRequest request, RoutingResponse response) { - RoutingExecutor routingExecutor = new RoutingExecutor(rootRoute, ctx, request, response); - filters.filter(request, response, routingExecutor); + RoutingExecutor routingExecutor = new RoutingExecutor(ctx, errorHandlers, rootRoute, request, response); + // we cannot throw an exception to the filters, as then the filter would not have information about actual status + // code, so error handling is done in routing executor and for each filter + filters.filter(ctx, request, response, routingExecutor); } @Override @@ -281,52 +288,45 @@ public Builder any(String pattern, Handler handler) { private static final class RoutingExecutor implements Runnable { private final ConnectionContext ctx; + private final ErrorHandlers errorHandlers; private final RoutingRequest request; private final RoutingResponse response; private final ServiceRoute rootRoute; - private RoutingExecutor(ServiceRoute rootRoute, ConnectionContext ctx, RoutingRequest request, RoutingResponse response) { - this.rootRoute = rootRoute; + private RoutingExecutor(ConnectionContext ctx, + ErrorHandlers errorHandlers, + ServiceRoute rootRoute, + RoutingRequest request, + RoutingResponse response) { this.ctx = ctx; + this.errorHandlers = errorHandlers; + this.rootRoute = rootRoute; this.request = request; this.response = response; } @Override public void run() { - try { - HttpPrologue prologue = request.prologue(); - response.resetRouting(); + response.resetRouting(); - RoutingResult result = RoutingResult.ROUTE; - int counter = 0; - while (result == RoutingResult.ROUTE) { - counter++; - if (counter == 10) { - LOGGER.log(System.Logger.Level.ERROR, "Rerouted more than 10 times. Will not attempt further routing"); - - throw HttpException.builder() - .request(HttpSimpleRequest.create(prologue, request.headers())) - .type(DirectHandler.EventType.INTERNAL_ERROR) - .build(); - } + RoutingResult result = RoutingResult.ROUTE; + int counter = 0; + while (result == RoutingResult.ROUTE) { + counter++; + if (counter == 10) { + LOGGER.log(System.Logger.Level.ERROR, "Rerouted more than 10 times. Will not attempt further routing"); - result = doRoute(ctx, request, response); + throw new HttpException("Too many reroutes", Http.Status.INTERNAL_SERVER_ERROR_500, true); } - // finished and done - if (result == RoutingResult.FINISH) { - return; - } - ctx.directHandlers().handle(HttpException.builder() - .request(request) - .response(response) - .type(DirectHandler.EventType.NOT_FOUND) - .message("Endpoint not found") - .build(), response); - } catch (HttpException e) { - ctx.directHandlers().handle(e, response); + result = doRoute(ctx, request, response); + } + + // finished and done + if (result == RoutingResult.FINISH) { + return; } + throw new NotFoundException("Endpoint not found"); } // todo we may have a lru cache (or most commonly used - weighted by number of usages) to @@ -370,11 +370,9 @@ private RoutingResult doRoute(ConnectionContext ctx, RoutingRequest request, Rou response.send(); return RoutingResult.FINISH; - } catch (CloseConnectionException | HttpException e) { + } catch (CloseConnectionException | RequestException | HttpException e) { throw e; } catch (Exception thrown) { - // TODO need to use error handler, error handler may reroute/next - // prevent infinite loops here if (thrown.getCause() instanceof SocketException se) { throw new UncheckedIOException(se); } @@ -390,15 +388,8 @@ private RoutingResult doRoute(ConnectionContext ctx, RoutingRequest request, Rou } // attempt to consume the request entity ctx.log(LOGGER, System.Logger.Level.WARNING, "Request failed", thrown); - // we must close connection, as we could not consume request - throw HttpException.builder() - .type(DirectHandler.EventType.INTERNAL_ERROR) - .cause(thrown) - .request(request) - .response(response) - .message(thrown.getMessage()) - .setKeepAlive(keepAlive) - .build(); + + throw new InternalServerException(thrown.getMessage(), thrown, keepAlive); } } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java index b21acf888bf..bdb3876b600 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponseBase.java @@ -25,9 +25,9 @@ import io.helidon.common.GenericType; import io.helidon.common.buffers.BufferData; -import io.helidon.common.http.DirectHandler; import io.helidon.common.http.HeadersServerRequest; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpPrologue; import io.helidon.common.uri.UriPath; import io.helidon.common.uri.UriQuery; @@ -106,12 +106,7 @@ public void send(Object entity) { mediaContext.writer(type, requestHeaders, headers()) .write(type, entity, outputStream, requestHeaders, this.headers()); } catch (IllegalArgumentException e) { - throw HttpException.builder() - .message(e.getMessage()) - .type(DirectHandler.EventType.OTHER) - .status(Http.Status.UNSUPPORTED_MEDIA_TYPE_415) - .request(HttpSimpleRequest.create(requestPrologue, requestHeaders)) - .build(); + throw new HttpException(e.getMessage(), Http.Status.UNSUPPORTED_MEDIA_TYPE_415, e, true); } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java index e0b4e1409a3..fd5f69f042c 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java @@ -23,6 +23,7 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; +import io.helidon.common.http.BadRequestException; import io.helidon.common.http.DirectHandler; import io.helidon.common.http.DirectHandler.EventType; import io.helidon.common.http.HeadersServerRequest; @@ -31,14 +32,15 @@ import io.helidon.common.http.Http; import io.helidon.common.http.Http.HeaderValues; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.InternalServerException; +import io.helidon.common.http.RequestException; import io.helidon.common.mapper.MapperException; import io.helidon.nima.http.encoding.ContentDecoder; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.http.HttpException; +import io.helidon.nima.webserver.http.DirectTransportRequest; import io.helidon.nima.webserver.http.HttpRouting; -import io.helidon.nima.webserver.http.HttpSimpleRequest; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; import io.helidon.nima.webserver.spi.ServerConnection; @@ -139,14 +141,22 @@ public void handle() throws InterruptedException { } } catch (CloseConnectionException | UncheckedIOException e) { throw e; - } catch (HttpException e) { - handleHttpException(e); + } catch (BadRequestException e) { + handleRequestException(RequestException.builder() + .message(e.getMessage()) + .cause(e) + .type(EventType.BAD_REQUEST) + .status(e.status()) + .setKeepAlive(e.keepAlive()) + .build()); + } catch (RequestException e) { + handleRequestException(e); } catch (Throwable e) { - handleHttpException(HttpException.builder() - .message("Internal error") - .type(EventType.INTERNAL_ERROR) - .cause(e) - .build()); + handleRequestException(RequestException.builder() + .message("Internal error") + .type(EventType.INTERNAL_ERROR) + .cause(e) + .build()); } } @@ -167,10 +177,10 @@ private BufferData readNextChunk(HttpPrologue prologue, HeadersWritable heade currentEntitySizeRead += chunkLength; if (maxPayloadSize != -1 && currentEntitySizeRead > maxPayloadSize) { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.BAD_REQUEST) .status(Http.Status.REQUEST_ENTITY_TOO_LARGE_413) - .request(HttpSimpleRequest.create(prologue, headers)) + .request(DirectTransportRequest.create(prologue, headers)) .setKeepAlive(false) .build(); } @@ -178,7 +188,7 @@ private BufferData readNextChunk(HttpPrologue prologue, HeadersWritable heade if (chunkLength == 0) { String end = reader.readLine(); if (!end.isEmpty()) { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.BAD_REQUEST) .message("Invalid terminating chunk") .build(); @@ -213,18 +223,18 @@ private void route(HttpPrologue prologue, HeadersWritable headers) { try { this.currentEntitySize = headers.get(Http.Header.CONTENT_LENGTH).value(long.class); if (maxPayloadSize != -1 && currentEntitySize > maxPayloadSize) { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.BAD_REQUEST) .status(Http.Status.REQUEST_ENTITY_TOO_LARGE_413) - .request(HttpSimpleRequest.create(prologue, headers)) + .request(DirectTransportRequest.create(prologue, headers)) .setKeepAlive(false) .build(); } entity = currentEntitySize == 0 ? EntityStyle.NONE : EntityStyle.LENGTH; } catch (MapperException e) { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.BAD_REQUEST) - .request(HttpSimpleRequest.create(prologue, headers)) + .request(DirectTransportRequest.create(prologue, headers)) .message("Content length is not a number") .cause(e) .build(); @@ -266,9 +276,9 @@ private void route(HttpPrologue prologue, HeadersWritable headers) { if (contentEncodingContext.contentDecodingSupported(contentEncoding)) { decoder = contentEncodingContext.decoder(contentEncoding); } else { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.BAD_REQUEST) - .request(HttpSimpleRequest.create(prologue, headers)) + .request(DirectTransportRequest.create(prologue, headers)) .message("Unsupported content encoding") .build(); } @@ -301,9 +311,9 @@ private void route(HttpPrologue prologue, HeadersWritable headers) { try { entityReadLatch.await(); } catch (InterruptedException e) { - throw HttpException.builder() + throw RequestException.builder() .type(EventType.INTERNAL_ERROR) - .request(HttpSimpleRequest.create(prologue, headers)) + .request(DirectTransportRequest.create(prologue, headers)) .message("Failed to wait for pipeline") .cause(e) .build(); @@ -322,25 +332,13 @@ private void consumeEntity(Http1ServerRequest request, Http1ServerResponse respo boolean keepAlive = request.content().consumed() && response.headers().contains(HeaderValues.CONNECTION_KEEP_ALIVE); // we must close connection, as we could not consume request if (!response.isSent()) { - throw HttpException.builder() - .type(EventType.INTERNAL_ERROR) - .cause(e) - .request(request) - .response(response) - .message(e.getMessage()) - .setKeepAlive(keepAlive) - .build(); + throw new InternalServerException(e.getMessage(), e, keepAlive); } throw new CloseConnectionException("Failed to consume request entity, must close", e); } } - private void handleHttpException(HttpException e) { - if (e.fullResponse().isPresent()) { - ctx.directHandlers().handle(e, e.fullResponse().get()); - return; - } - + private void handleRequestException(RequestException e) { DirectHandler handler = ctx.directHandlers().handler(e.eventType()); DirectHandler.TransportResponse response = handler.handle(e.request(), e.eventType(), @@ -362,6 +360,8 @@ private void handleHttpException(HttpException e) { buffer.write(message); } + sendListener.headers(ctx, headers); + sendListener.data(ctx, buffer); writer.write(buffer); if (response.status() == Http.Status.INTERNAL_SERVER_ERROR_500) { 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 0746cd43894..33f6ba187fb 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 @@ -21,8 +21,8 @@ import io.helidon.common.http.HeadersWritable; import io.helidon.common.http.Http1HeadersParser; import io.helidon.common.http.HttpPrologue; -import io.helidon.nima.webserver.http.HttpException; -import io.helidon.nima.webserver.http.HttpSimpleRequest; +import io.helidon.common.http.RequestException; +import io.helidon.nima.webserver.http.DirectTransportRequest; /** * HTTP/1 headers reader. @@ -56,9 +56,9 @@ public HeadersWritable readHeaders(HttpPrologue prologue) { try { return Http1HeadersParser.readHeaders(reader, maxHeadersSize, validateHeaders); } catch (IllegalStateException | IllegalArgumentException e) { - throw HttpException.builder() + throw RequestException.builder() .type(DirectHandler.EventType.BAD_REQUEST) - .request(HttpSimpleRequest.create(prologue, HeadersWritable.create())) + .request(DirectTransportRequest.create(prologue, HeadersWritable.create())) .message(e.getMessage()) .cause(e) .build(); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java index 2fb96163191..84b0e6cb853 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java @@ -23,9 +23,9 @@ import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Http; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.RequestException; import io.helidon.nima.webserver.CloseConnectionException; -import io.helidon.nima.webserver.http.HttpException; -import io.helidon.nima.webserver.http.HttpSimpleRequest; +import io.helidon.nima.webserver.http.DirectTransportRequest; /** * HTTP 1 prologue parsing support. @@ -63,16 +63,16 @@ public HttpPrologue readPrologue() { } } - private static HttpException badRequest(String message, String method, String path, String protocol, String version) { + private static RequestException badRequest(String message, String method, String path, String protocol, String version) { String protocolAndVersion; if (protocol.isBlank() && version.isBlank()) { protocolAndVersion = ""; } else { protocolAndVersion = protocol + "/" + version; } - return HttpException.builder() + return RequestException.builder() .type(DirectHandler.EventType.BAD_REQUEST) - .request(HttpSimpleRequest.create(protocolAndVersion, method, path)) + .request(DirectTransportRequest.create(protocolAndVersion, method, path)) .message(message) .build(); } @@ -113,11 +113,11 @@ private HttpPrologue doRead() { if (secondSpace < 0) { throw badRequest("Invalid prologue" + reader.debugDataHex(), method.text(), "", "", ""); } else if (secondSpace == maxLen) { - throw HttpException.builder() + throw RequestException.builder() .message("Request URI too long.") .type(DirectHandler.EventType.BAD_REQUEST) .status(Http.Status.REQUEST_URI_TOO_LONG_414) - .request(HttpSimpleRequest.create("", method.text(), reader.readAsciiString(secondSpace))) + .request(DirectTransportRequest.create("", method.text(), reader.readAsciiString(secondSpace))) .build(); } path = reader.readAsciiString(secondSpace); @@ -132,7 +132,7 @@ private HttpPrologue doRead() { protocol = reader.readAsciiString(eol); reader.skip(2); // \r\n } catch (DataReader.IncorrectNewLineException e) { - throw HttpException.builder() + throw RequestException.builder() .message("Invalid prologue: " + e.getMessage()) .type(DirectHandler.EventType.BAD_REQUEST) .cause(e) diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java index 79c904257cf..707fa8611b6 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java @@ -32,8 +32,8 @@ import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http.HeaderName; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.RequestException; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.http.HttpException; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; import io.helidon.nima.webserver.spi.ServerConnection; @@ -105,7 +105,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, He } if (!SUPPORTED_VERSION.equals(version)) { - throw HttpException.builder() + throw RequestException.builder() .type(DirectHandler.EventType.BAD_REQUEST) .message("Unsupported WebSocket Version") .header(SUPPORTED_VERSION_HEADER) @@ -123,7 +123,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, He if (headers.contains(Header.ORIGIN)) { String origin = headers.get(Header.ORIGIN).value(); if (!origins.contains(origin)) { - throw HttpException.builder() + throw RequestException.builder() .message("Invalid Origin") .type(DirectHandler.EventType.FORBIDDEN) .build(); @@ -155,7 +155,7 @@ private String hash(ConnectionContext ctx, String wsKey) { been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection. */ - throw HttpException.builder() + throw RequestException.builder() .type(DirectHandler.EventType.BAD_REQUEST) .message("Invalid Sec-WebSocket-Key header") .build(); diff --git a/reactive/webserver/jersey/src/main/java/io/helidon/reactive/webserver/jersey/JerseySupport.java b/reactive/webserver/jersey/src/main/java/io/helidon/reactive/webserver/jersey/JerseySupport.java index 7286bfbc151..c96dbce6506 100644 --- a/reactive/webserver/jersey/src/main/java/io/helidon/reactive/webserver/jersey/JerseySupport.java +++ b/reactive/webserver/jersey/src/main/java/io/helidon/reactive/webserver/jersey/JerseySupport.java @@ -37,10 +37,10 @@ import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.config.Config; import io.helidon.config.ConfigValue; import io.helidon.reactive.webserver.Handler; -import io.helidon.reactive.webserver.HttpException; import io.helidon.reactive.webserver.KeyPerformanceIndicatorSupport; import io.helidon.reactive.webserver.Routing; import io.helidon.reactive.webserver.ServerRequest; diff --git a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/ClassPathContentHandler.java b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/ClassPathContentHandler.java index af23817934b..71d983eaf80 100644 --- a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/ClassPathContentHandler.java +++ b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/ClassPathContentHandler.java @@ -37,8 +37,8 @@ import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.reactive.IoMulti; -import io.helidon.reactive.webserver.HttpException; import io.helidon.reactive.webserver.ServerRequest; import io.helidon.reactive.webserver.ServerResponse; diff --git a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/FileBasedContentHandler.java b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/FileBasedContentHandler.java index 74797b5db76..16a12e26e88 100644 --- a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/FileBasedContentHandler.java +++ b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/FileBasedContentHandler.java @@ -27,12 +27,12 @@ import java.util.logging.Logger; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpMediaType; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.reactive.media.common.DefaultMediaSupport; import io.helidon.reactive.media.common.MessageBodyWriter; -import io.helidon.reactive.webserver.HttpException; import io.helidon.reactive.webserver.RequestHeaders; import io.helidon.reactive.webserver.ResponseHeaders; import io.helidon.reactive.webserver.ServerRequest; @@ -61,12 +61,12 @@ static String fileName(Path path) { } /** - * Find welcome file in provided directory or throw not found {@link io.helidon.reactive.webserver.HttpException}. + * Find welcome file in provided directory or throw not found {@link io.helidon.common.http.HttpException}. * * @param directory a directory to find in * @param name welcome file name * @return a path of the welcome file - * @throws io.helidon.reactive.webserver.HttpException if welcome file doesn't exists + * @throws io.helidon.common.http.HttpException if welcome file doesn't exists */ private static Path resolveWelcomeFile(Path directory, String name) { throwNotFoundIf(name == null || name.isEmpty()); diff --git a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandler.java b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandler.java index 18376b13357..a760198fd14 100644 --- a/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandler.java +++ b/reactive/webserver/static-content/src/main/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandler.java @@ -27,7 +27,7 @@ import java.util.logging.Logger; import io.helidon.common.http.Http; -import io.helidon.reactive.webserver.HttpException; +import io.helidon.common.http.HttpException; import io.helidon.reactive.webserver.RequestHeaders; import io.helidon.reactive.webserver.ResponseHeaders; import io.helidon.reactive.webserver.Routing; @@ -128,7 +128,7 @@ void handle(Http.Method method, ServerRequest request, ServerResponse response) * @param response an HTTP response * @return {@code true} only if static content was found and processed. * @throws java.io.IOException if resource is not acceptable - * @throws io.helidon.reactive.webserver.HttpException if some known WEB error + * @throws io.helidon.common.http.HttpException if some known WEB error */ abstract boolean doHandle(Http.Method method, String requestedPath, ServerRequest request, ServerResponse response) throws IOException, URISyntaxException; @@ -140,7 +140,7 @@ abstract boolean doHandle(Http.Method method, String requestedPath, ServerReques * @param etag the proposed ETag. If {@code null} then method returns false * @param requestHeaders an HTTP request headers * @param responseHeaders an HTTP response headers - * @throws io.helidon.reactive.webserver.HttpException if ETag is checked + * @throws io.helidon.common.http.HttpException if ETag is checked */ static void processEtag(String etag, RequestHeaders requestHeaders, ResponseHeaders responseHeaders) { if (etag == null || etag.isEmpty()) { @@ -198,7 +198,7 @@ private static String unquoteETag(String etag) { * @param modified the last modification instance. If {@code null} then method just returns {@code false}. * @param requestHeaders an HTTP request headers * @param responseHeaders an HTTP response headers - * @throws io.helidon.reactive.webserver.HttpException if (un)modify since header is checked + * @throws io.helidon.common.http.HttpException if (un)modify since header is checked */ static void processModifyHeaders(Instant modified, RequestHeaders requestHeaders, ResponseHeaders responseHeaders) { if (modified == null) { @@ -223,10 +223,10 @@ static void processModifyHeaders(Instant modified, RequestHeaders requestHeaders } /** - * If provided {@code condition} is {@code true} then throws not found {@link io.helidon.reactive.webserver.HttpException}. + * If provided {@code condition} is {@code true} then throws not found {@link io.helidon.common.http.HttpException}. * * @param condition if condition is true then throws an exception otherwise not - * @throws io.helidon.reactive.webserver.HttpException if {@code condition} parameter is {@code true}. + * @throws io.helidon.common.http.HttpException if {@code condition} parameter is {@code true}. */ static void throwNotFoundIf(boolean condition) { if (condition) { diff --git a/reactive/webserver/static-content/src/test/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandlerTest.java b/reactive/webserver/static-content/src/test/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandlerTest.java index cfdf6585b4a..a70fc11869d 100644 --- a/reactive/webserver/static-content/src/test/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandlerTest.java +++ b/reactive/webserver/static-content/src/test/java/io/helidon/reactive/webserver/staticcontent/StaticContentHandlerTest.java @@ -27,7 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger; import io.helidon.common.http.Http; -import io.helidon.reactive.webserver.HttpException; +import io.helidon.common.http.HttpException; import io.helidon.reactive.webserver.RequestHeaders; import io.helidon.reactive.webserver.ResponseHeaders; import io.helidon.reactive.webserver.ServerRequest; diff --git a/reactive/webserver/test-support/src/test/java/io/helidon/reactive/webserver/testsupport/TestClientTest.java b/reactive/webserver/test-support/src/test/java/io/helidon/reactive/webserver/testsupport/TestClientTest.java index 61670929619..00c9fc67938 100644 --- a/reactive/webserver/test-support/src/test/java/io/helidon/reactive/webserver/testsupport/TestClientTest.java +++ b/reactive/webserver/test-support/src/test/java/io/helidon/reactive/webserver/testsupport/TestClientTest.java @@ -20,8 +20,8 @@ import java.util.concurrent.TimeUnit; import io.helidon.common.http.Http; -import io.helidon.reactive.webserver.HttpException; -import io.helidon.reactive.webserver.NotFoundException; +import io.helidon.common.http.HttpException; +import io.helidon.common.http.NotFoundException; import io.helidon.reactive.webserver.Routing; import org.junit.jupiter.api.Test; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java index 0eadab6cf2d..55a379a06e5 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ForwardingHandler.java @@ -32,6 +32,7 @@ import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; +import io.helidon.common.http.BadRequestException; import io.helidon.common.http.DirectHandler; import io.helidon.common.http.DirectHandler.TransportResponse; import io.helidon.common.http.HeadersServerRequest; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java index 957cff71ee7..f38f0ed8271 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/RequestRouting.java @@ -35,7 +35,9 @@ import io.helidon.common.context.Contexts; import io.helidon.common.http.HtmlEncoder; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpMediaType; +import io.helidon.common.http.NotFoundException; import io.helidon.tracing.Span; import io.helidon.tracing.SpanContext; import io.helidon.tracing.Tracer; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/Response.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/Response.java index eb7558243d7..6d028d3a4ad 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/Response.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/Response.java @@ -27,6 +27,7 @@ import io.helidon.common.GenericType; import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.http.HttpMediaType; import io.helidon.common.media.type.MediaType; import io.helidon.common.reactive.Single; diff --git a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ServerRequest.java b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ServerRequest.java index b967e761c42..b4b99c77af2 100644 --- a/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ServerRequest.java +++ b/reactive/webserver/webserver/src/main/java/io/helidon/reactive/webserver/ServerRequest.java @@ -25,6 +25,7 @@ import io.helidon.common.context.Context; import io.helidon.common.http.Http; +import io.helidon.common.http.HttpException; import io.helidon.common.reactive.Single; import io.helidon.common.uri.UriQuery; import io.helidon.reactive.media.common.MessageBodyReadableContent; diff --git a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseTest.java b/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseTest.java index 0c2a8977cba..c880467f3d6 100644 --- a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseTest.java +++ b/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseTest.java @@ -27,6 +27,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import io.helidon.common.http.BadRequestException; import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; import io.helidon.common.reactive.Multi; diff --git a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseV2ApiTest.java b/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseV2ApiTest.java index ab02c96e70a..1e1807dd46e 100644 --- a/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseV2ApiTest.java +++ b/reactive/webserver/webserver/src/test/java/io/helidon/reactive/webserver/BytesReuseV2ApiTest.java @@ -27,6 +27,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import io.helidon.common.http.BadRequestException; import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; import io.helidon.common.reactive.Multi;