Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http exception refactoring #4804

Merged
merged 3 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}.
Expand Down Expand Up @@ -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}.
*
Expand All @@ -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;
}

/**
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -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);
}

/**
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -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<ServerResponse> fullResponse() {
return Optional.ofNullable(fullResponse);
}

/**
* Whether to attempt to keep connection alive.
*
Expand All @@ -114,23 +98,22 @@ 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<Builder, HttpException> {
public static class Builder implements io.helidon.common.Builder<Builder, RequestException> {
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();

private Builder() {
}

@Override
public HttpException build() {
public RequestException build() {
if (message == null) {
message = "";
}
Expand All @@ -140,7 +123,7 @@ public HttpException build() {
if (type == null) {
type(DirectHandler.EventType.INTERNAL_ERROR);
}
return new HttpException(this);
return new RequestException(this);
}

/**
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions docs-internal/http-exceptions.md
Original file line number Diff line number Diff line change
@@ -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
Loading