Skip to content

Commit

Permalink
resolves #853 print an explicit error message when the URI is too lon…
Browse files Browse the repository at this point in the history
…g (414)
  • Loading branch information
ggrossetie committed Aug 22, 2021
1 parent dbe4228 commit 5366bc2
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 56 deletions.
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

0 comments on commit 5366bc2

Please sign in to comment.