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

resolves #853 print an explicit error message when the URI is too long (414) #869

Merged
merged 1 commit into from
Aug 27, 2021
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
12 changes: 10 additions & 2 deletions server/src/main/java/io/kroki/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.kroki.server.action.Commander;
import io.kroki.server.error.ErrorHandler;
import io.kroki.server.error.InvalidRequestHandler;
import io.kroki.server.log.Logging;
import io.kroki.server.service.Blockdiag;
import io.kroki.server.service.Bpmn;
Expand Down Expand Up @@ -140,13 +141,18 @@ static void start(Vertx vertx, JsonObject config, Handler<AsyncResult<HttpServer
// Default route
Route route = router.route("/*");
route.handler(routingContext -> routingContext.fail(404));
route.failureHandler(new ErrorHandler(vertx, false));
ErrorHandler errorHandler = new ErrorHandler(vertx, false);
route.failureHandler(errorHandler);

server.requestHandler(router).listen(getListenAddress(config), listenHandler);
server
.invalidRequestHandler(new InvalidRequestHandler(errorHandler, serverOptions.getMaxInitialLineLength()))
.requestHandler(router)
.listen(getListenAddress(config), listenHandler);
}

/**
* Get the address the service will listen on.
*
* @param config configuration
* @return the address
*/
Expand Down Expand Up @@ -184,6 +190,7 @@ static SocketAddress getListenAddress(JsonObject config) {

/**
* Get the address the service will listen on from IPv6.
*
* @param listen listen value
* @return the address
*/
Expand All @@ -199,6 +206,7 @@ private static SocketAddress getIPv6ListenAddress(String listen) {

/**
* Get the port the service will listen on.
*
* @param config configuration
* @return the port
*/
Expand Down
64 changes: 64 additions & 0 deletions server/src/main/java/io/kroki/server/error/ErrorContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.kroki.server.error;

import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.MIMEHeader;
import io.vertx.ext.web.impl.ParsableMIMEValue;

import java.util.List;
import java.util.stream.Collectors;

public class ErrorContext {

private final List<MIMEHeader> acceptableMimes;
private final HttpServerRequest request;
private final HttpServerResponse response;
private final ErrorInfo errorInfo;
private final String statusMessage;

public ErrorContext(HttpServerRequest request, HttpServerResponse response, String statusMessage, ErrorInfo errorInfo) {
this.acceptableMimes = request.headers().getAll("accept").stream().map(ParsableMIMEValue::new).collect(Collectors.toList());
this.request = request;
this.response = response;
// no new lines are allowed in the status message
this.statusMessage = statusMessage.replaceAll("[\\r\\n]", " ");
this.errorInfo = errorInfo;
}

public List<MIMEHeader> getAcceptableMimes() {
return acceptableMimes;
}

public HttpServerRequest getRequest() {
return request;
}

public HttpServerResponse getResponse() {
return response;
}

public String getStatusMessage() {
return statusMessage;
}

public ErrorInfo getErrorInfo() {
return errorInfo;
}


public Throwable getFailure() {
return errorInfo.getFailure();
}

public int getErrorCode() {
return errorInfo.getCode();
}

public String getErrorMessage() {
return errorInfo.getMessage();
}

public String getHtmlErrorMessage() {
return errorInfo.getHtmlMessage();
}
}
59 changes: 29 additions & 30 deletions server/src/main/java/io/kroki/server/error/ErrorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,56 +84,55 @@ public void handle(RoutingContext context) {
if (statusMessage == null) {
statusMessage = errorMessage;
}
// no new lines are allowed in the status message
response.setStatusMessage(statusMessage.replaceAll("[\\r\\n]", " "));
logging.error(context, errorCode, errorMessage);
answerWithError(context, errorCode, errorMessage, htmlErrorMessage);
handleError(new ErrorContext(context.request(), context.response(), statusMessage, new ErrorInfo(context.failure(), errorCode, errorMessage, htmlErrorMessage)));
}

private void answerWithError(RoutingContext context, int errorCode, String errorMessage, String htmlErrorMessage) {
context.response().setStatusCode(errorCode);
if (!sendErrorResponseMIME(context, errorCode, errorMessage, htmlErrorMessage) && !sendErrorAcceptMIME(context, errorCode, errorMessage, htmlErrorMessage)) {
public void handleError(ErrorContext errorContext) {
HttpServerResponse response = errorContext.getResponse();
response.setStatusMessage(errorContext.getStatusMessage());
logging.error(errorContext.getRequest(), errorContext.getErrorInfo());
response.setStatusCode(errorContext.getErrorCode());
ErrorInfo errorInfo = errorContext.getErrorInfo();
if (!sendErrorResponseMIME(response, errorInfo) && !sendErrorAcceptMIME(response, errorContext.getAcceptableMimes(), errorInfo)) {
// fallback plain/text
sendError(context, "text/plain", errorCode, errorMessage, htmlErrorMessage);
sendError(response, "text/plain", errorInfo);
}
}

private boolean sendErrorResponseMIME(RoutingContext context, int errorCode, String errorMessage, String htmlErrorMessage) {
private boolean sendErrorResponseMIME(HttpServerResponse response, ErrorInfo errorInfo) {
// does the response already set the mime type?
String mime = context.response().headers().get(HttpHeaders.CONTENT_TYPE);
return mime != null && sendError(context, mime, errorCode, errorMessage, htmlErrorMessage);
String mime = response.headers().get(HttpHeaders.CONTENT_TYPE);
return mime != null && sendError(response, mime, errorInfo);
}

private boolean sendErrorAcceptMIME(RoutingContext context, int errorCode, String errorMessage, String htmlErrorMessage) {
private boolean sendErrorAcceptMIME(HttpServerResponse response, List<MIMEHeader> acceptableMimes, ErrorInfo errorInfo) {
// respect the client accept order
List<MIMEHeader> acceptableMimes = context.parsedHeaders().accept();
for (MIMEHeader accept : acceptableMimes) {
if (sendError(context, accept.value(), errorCode, errorMessage, htmlErrorMessage)) {
if (sendError(response, accept.value(), errorInfo)) {
return true;
}
}
return false;
}

private boolean sendError(RoutingContext context, String mime, int errorCode, String errorMessage, String htmlErrorMessage) {
private boolean sendError(HttpServerResponse response, String mime, ErrorInfo errorInfo) {
Throwable failure = errorInfo.getFailure();
int errorCode = errorInfo.getCode();
String errorMessage = errorInfo.getMessage();
final String title = "\uD83E\uDD16 bip... bip... something wrong happened!";
HttpServerResponse response = context.response();
if (mime.startsWith("text/html")) {
StringBuilder stack = new StringBuilder();
if (context.failure() != null && displayExceptionDetails) {
for (StackTraceElement elem : context.failure().getStackTrace()) {
if (failure != null && displayExceptionDetails) {
for (StackTraceElement elem : failure.getStackTrace()) {
stack.append("<li>").append(elem).append("</li>");
}
}
response.putHeader(HttpHeaders.CONTENT_TYPE, "text/html");
if (htmlErrorMessage == null) {
htmlErrorMessage = errorMessage;
}
response.end(
errorTemplate
.replace("{title}", title)
.replace("{errorCode}", Integer.toString(errorCode))
.replace("{errorMessage}", htmlErrorMessage)
.replace("{errorMessage}", errorInfo.getHtmlMessage())
.replace("{stackTrace}", stack.toString())
);
return true;
Expand All @@ -142,9 +141,9 @@ private boolean sendError(RoutingContext context, String mime, int errorCode, St
if (mime.startsWith("application/json")) {
JsonObject jsonError = new JsonObject();
jsonError.put("error", new JsonObject().put("code", errorCode).put("message", errorMessage));
if (context.failure() != null && displayExceptionDetails) {
if (failure != null && displayExceptionDetails) {
JsonArray stack = new JsonArray();
for (StackTraceElement elem : context.failure().getStackTrace()) {
for (StackTraceElement elem : failure.getStackTrace()) {
stack.add(elem.toString());
}
jsonError.put("stack", stack);
Expand All @@ -155,14 +154,14 @@ private boolean sendError(RoutingContext context, String mime, int errorCode, St
}

if (mime.startsWith("text/plain")) {
String completeErrorMessage = getCompleteErrorMessage(context, errorCode, errorMessage);
String completeErrorMessage = getCompleteErrorMessage(failure, errorCode, errorMessage);
response.putHeader(HttpHeaders.CONTENT_TYPE, "text/plain");
response.end(completeErrorMessage);
return true;
}

if (mime.startsWith("image/svg+xml")) {
String completeErrorMessage = getCompleteErrorMessage(context, errorCode, errorMessage);
String completeErrorMessage = getCompleteErrorMessage(failure, errorCode, errorMessage);
try {
String svgImage = ErrorImage.buildSVGImage(completeErrorMessage).getSource();
response.putHeader(HttpHeaders.CONTENT_TYPE, "image/svg+xml");
Expand All @@ -175,7 +174,7 @@ private boolean sendError(RoutingContext context, String mime, int errorCode, St
}

if (mime.startsWith("image/png") || mime.startsWith("image/*")) {
String completeErrorMessage = getCompleteErrorMessage(context, errorCode, errorMessage);
String completeErrorMessage = getCompleteErrorMessage(failure, errorCode, errorMessage);
try ( ByteArrayOutputStream output = new ByteArrayOutputStream()) {
BufferedImage bufferedImage = ErrorImage.buildPNGImage(completeErrorMessage);
ImageIO.write(bufferedImage, "png", output);
Expand All @@ -191,14 +190,14 @@ private boolean sendError(RoutingContext context, String mime, int errorCode, St
return false;
}

private String getCompleteErrorMessage(RoutingContext context, int errorCode, String errorMessage) {
private String getCompleteErrorMessage(Throwable failure, int errorCode, String errorMessage) {
StringBuilder sb = new StringBuilder();
sb.append("Error ");
sb.append(errorCode);
sb.append(": ");
sb.append(errorMessage);
if (context.failure() != null && displayExceptionDetails) {
for (StackTraceElement elem : context.failure().getStackTrace()) {
if (failure != null && displayExceptionDetails) {
for (StackTraceElement elem : failure.getStackTrace()) {
sb.append("\tat ").append(elem).append("\n");
}
}
Expand Down
35 changes: 35 additions & 0 deletions server/src/main/java/io/kroki/server/error/ErrorInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.kroki.server.error;

public class ErrorInfo {

private final Throwable failure;
private final int code;
private final String message;
private final String htmlMessage;

public ErrorInfo(Throwable failure, int code, String message, String htmlMessage) {
this.failure = failure;
this.code = code;
this.message = message;
this.htmlMessage = htmlMessage;
}

public Throwable getFailure() {
return failure;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}

public String getHtmlMessage() {
if (htmlMessage == null) {
return message;
}
return htmlMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.kroki.server.error;

import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.http.HttpVersion;

public class InvalidRequestHandler implements Handler<HttpServerRequest> {

private final ErrorHandler errorHandler;
private final int maxInitialLineLength;

public InvalidRequestHandler(ErrorHandler errorHandler, int maxInitialLineLength) {
this.errorHandler = errorHandler;
this.maxInitialLineLength = maxInitialLineLength;
}

/**
* Copied and adapted from {@link HttpServerRequest#DEFAULT_INVALID_REQUEST_HANDLER} to print an explicit error message when the URL is too long.
*/
@Override
public void handle(HttpServerRequest httpServerRequest) {
DecoderResult result = httpServerRequest.decoderResult();
Throwable cause = result.cause();
HttpResponseStatus status = null;
if (cause instanceof TooLongFrameException) {
HttpServerResponse response = httpServerRequest.response();
String causeMsg = cause.getMessage();
if (causeMsg.startsWith("An HTTP line is larger than")) {
HttpResponseStatus responseStatus = HttpResponseStatus.REQUEST_URI_TOO_LONG;
this.errorHandler.handleError(new ErrorContext(
httpServerRequest,
response,
responseStatus.reasonPhrase(),
new ErrorInfo(
cause,
responseStatus.code(),
"The request URI's length exceeds " + maxInitialLineLength + ". You can update this value by setting KROKI_MAX_URI_LENGTH environment variable. Please read: https://docs.kroki.io/kroki/setup/configuration/#_max_uri_length for more information.",
null
)
));
return;
} else if (causeMsg.startsWith("HTTP header is larger than")) {
status = HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE;
}
response.setStatusMessage(causeMsg.replaceAll("[\\r\\n]", " "));
}
if (status == null && HttpMethod.GET == httpServerRequest.method() &&
HttpVersion.HTTP_1_0 == httpServerRequest.version() && "/bad-request".equals(httpServerRequest.uri())) {
// Matches Netty's specific HttpRequest for invalid messages
status = HttpResponseStatus.BAD_REQUEST;
}
if (status != null) {
httpServerRequest.response().setStatusCode(status.code()).end();
} else {
httpServerRequest.connection().close();
}
}
}
10 changes: 5 additions & 5 deletions server/src/main/java/io/kroki/server/log/Logging.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.kroki.server.log;

import io.kroki.server.error.ErrorInfo;
import io.kroki.server.format.FileFormat;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
Expand Down Expand Up @@ -94,15 +95,13 @@ public void reroute(String from, String to, String source, FileFormat fileFormat
}
}

public void error(RoutingContext routingContext, int errorCode, String errorMessage) {
HttpServerRequest request = routingContext.request();
Throwable failure = routingContext.failure();
public void error(HttpServerRequest request, ErrorInfo errorInfo) {
try {
MDC.put("action", "error");
MDC.put("method", request.method().toString());
MDC.put("path", request.path());
MDC.put("error_code", String.valueOf(errorCode));
MDC.put("error_message", errorMessage);
MDC.put("error_code", String.valueOf(errorInfo.getCode()));
MDC.put("error_message", errorInfo.getMessage());
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
MDC.put("user_agent", userAgent);
Expand All @@ -111,6 +110,7 @@ public void error(RoutingContext routingContext, int errorCode, String errorMess
if (referer != null) {
MDC.put("referrer", referer);
}
Throwable failure = errorInfo.getFailure();
if (failure != null) {
MDC.put("failure_class_name", failure.getClass().getName());
logger.error("An error occurred", failure);
Expand Down
Loading