in languagetool-server/src/main/java/org/languagetool/server/LanguageToolHttpHandler.java [93:284]
public void handle(HttpExchange httpExchange) throws IOException {
long startTime = System.currentTimeMillis();
String remoteAddress = null;
Map<String, String> parameters = new HashMap<>();
int reqId = reqCounter.incrementRequestCount();
ServerMetricsCollector.getInstance().logRequest();
boolean incrementHandleCount = false;
String requestId = getRequestId(httpExchange);
MDC.MDCCloseable mdcRequestID = MDC.putCloseable("rID", requestId);
Attributes attributes = Attributes.builder()
.put(SemanticAttributes.HTTP_METHOD, httpExchange.getRequestMethod())
.put(SemanticAttributes.HTTP_ROUTE, httpExchange.getRequestURI().getRawPath())
.put("http.path_group", httpExchange.getRequestURI().getRawPath())
.put("request.id", requestId)
.build();
Span globalSpan = TelemetryProvider.INSTANCE.createSpan("handle-http-request", attributes);
try (Scope scope = globalSpan.makeCurrent()) {
URI requestedUri = httpExchange.getRequestURI();
String path = requestedUri.getRawPath();
logger.info("Handling {} {}", httpExchange.getRequestMethod(), path);
if (config.getServerURL() != null) {
path = config.getServerURL().relativize(new URI(requestedUri.getPath())).getRawPath();
if (!path.startsWith("/")) {
path = "/" + path;
}
}
if (path.startsWith("/v2/stop") && config.isStoppable()) {
logger.warn("Stopping server by external command");
httpServer.stop();
return;
}
if (path.startsWith("/v2/")) {
// healthcheck should come before other limit checks (requests per time etc.), to be sure it works:
String pathWithoutVersion = path.substring("/v2/".length());
if (pathWithoutVersion.equals("healthcheck")) {
String message = "Healthcheck failed: There are currently too many parallel requests.";
if (workQueueFull(httpExchange, parameters, message) || textCheckerQueueFull(httpExchange, message)) {
ServerMetricsCollector.getInstance().logFailedHealthcheck();
return;
} else {
httpExchange.getResponseHeaders().set("Content-Type", "text/plain");
String requestMethod = httpExchange.getRequestMethod();
if ("HEAD".equalsIgnoreCase(requestMethod)) {
// Send HTTP 200 without content
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1);
} else {
String ok = "OK";
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, ok.getBytes(ENCODING).length);
httpExchange.getResponseBody().write(ok.getBytes(ENCODING));
}
ServerMetricsCollector.getInstance().logResponse(HttpURLConnection.HTTP_OK);
return;
}
}
}
String referrer = httpExchange.getRequestHeaders().getFirst("Referer");
String origin = httpExchange.getRequestHeaders().getFirst("Origin"); // Referer can be turned off with meta tags, so also check this
for (String ref : config.getBlockedReferrers()) {
String errorMessage = null;
if (ref != null && !ref.isEmpty()) {
if (referrer != null && ServerTools.siteMatches(referrer, ref)) {
errorMessage = "Error: Access with referrer " + referrer + " denied.";
} else if (origin != null && ServerTools.siteMatches(origin, ref)) {
errorMessage = "Error: Access with origin " + origin + " denied.";
}
}
if (errorMessage != null) {
sendError(httpExchange, HttpURLConnection.HTTP_FORBIDDEN, errorMessage);
logError(errorMessage, HttpURLConnection.HTTP_FORBIDDEN, parameters, httpExchange);
ServerMetricsCollector.getInstance().logResponse(HttpURLConnection.HTTP_FORBIDDEN);
return;
}
}
String origAddress = httpExchange.getRemoteAddress().getAddress().getHostAddress();
String realAddressOrNull = getRealRemoteAddressOrNull(httpExchange, config);
remoteAddress = realAddressOrNull != null ? realAddressOrNull : origAddress;
reqCounter.incrementHandleCount(remoteAddress, reqId);
incrementHandleCount = true;
// According to the Javadoc, "Closing an exchange without consuming all of the request body is
// not an error but may make the underlying TCP connection unusable for following exchanges.",
// so we consume the request now, even before checking for request limits:
parameters = getRequestQuery(httpExchange, requestedUri);
if (requestLimiter != null && limitPath(path) && !allowSkipRequestLimit(httpExchange.getRequestHeaders())) {
try {
UserLimits userLimits = ServerTools.getUserLimits(parameters, config);
requestLimiter.checkAccess(remoteAddress, parameters, httpExchange.getRequestHeaders(), userLimits);
} catch (TooManyRequestsException e) {
String errorMessage = "Error: Access from " + remoteAddress + " denied: " + e.getMessage();
int code = 429; // too many requests
sendError(httpExchange, code, errorMessage);
// already logged via DatabaseAccessLimitLogEntry
logError(errorMessage, code, parameters, httpExchange, false);
return;
}
}
if (errorRequestLimiter != null &&
!allowSkipRequestLimit(httpExchange.getRequestHeaders()) &&
!errorRequestLimiter.wouldAccessBeOkay(remoteAddress, parameters, httpExchange.getRequestHeaders())) {
String textSizeMessage = getTextOrDataSizeMessage(parameters);
String errorMessage = "Error: Access from " + remoteAddress + " denied - too many recent timeouts. " +
textSizeMessage +
" Allowed maximum timeouts: " + errorRequestLimiter.getRequestLimit() +
" per " + errorRequestLimiter.getRequestLimitPeriodInSeconds() + " seconds";
int code = 429; // too many requests
sendError(httpExchange, code, errorMessage);
logError(errorMessage, code, parameters, httpExchange);
return;
}
if (workQueueFull(httpExchange, parameters, "Error: There are currently too many parallel requests. Please try again later.")) {
ServerMetricsCollector.getInstance().logRequestError(ServerMetricsCollector.RequestErrorType.QUEUE_FULL);
return;
}
if (allowedIps == null || allowedIps.contains(origAddress)) {
if (path.startsWith("/v2/")) {
ApiV2 apiV2 = new ApiV2(textCheckerV2, config.getAllowOriginUrl());
String pathWithoutVersion = path.substring("/v2/".length());
final Map<String, String> finalParameters = parameters;
final String finalRemoteAddress = remoteAddress;
TelemetryProvider.INSTANCE.createSpan("/v2", Attributes.empty(), () -> apiV2.handleRequest(pathWithoutVersion, httpExchange, finalParameters, errorRequestLimiter, finalRemoteAddress, config));
} else if (path.endsWith("/Languages")) {
throw new BadRequestException("You're using an old version of our API that's not supported anymore. Please see " + API_DOC_URL);
} else if (path.equals("/")) {
throw new BadRequestException("Missing arguments for LanguageTool API. Please see " + API_DOC_URL);
} else if (path.contains("/v2/")) {
throw new BadRequestException("You have '/v2/' in your path, but not at the root. Try an URL like 'http://server/v2/...' ");
} else if (path.equals("/favicon.ico")) {
sendError(httpExchange, HttpURLConnection.HTTP_NOT_FOUND, "Not found");
} else {
throw new BadRequestException("This is the LanguageTool API. You have not specified any parameters. Please see " + API_DOC_URL);
}
} else {
String errorMessage = "Error: Access from " + StringTools.escapeXML(origAddress) + " denied";
sendError(httpExchange, HttpURLConnection.HTTP_FORBIDDEN, errorMessage);
throw new RuntimeException(errorMessage);
}
} catch (Exception e) {
String response;
int errorCode;
boolean textLoggingAllowed = false;
boolean logStacktrace = true;
Throwable rootCause = ExceptionUtils.getRootCause(e);
if (e instanceof TextTooLongException || rootCause instanceof TextTooLongException) {
errorCode = HttpURLConnection.HTTP_ENTITY_TOO_LARGE;
response = e.getMessage();
logStacktrace = false;
} else if (e instanceof ErrorRateTooHighException || rootCause instanceof ErrorRateTooHighException) {
errorCode = HttpURLConnection.HTTP_BAD_REQUEST;
response = ExceptionUtils.getRootCause(e).getMessage();
logStacktrace = false;
} else if (hasCause(e, AuthException.class)) {
errorCode = HttpURLConnection.HTTP_FORBIDDEN;
response = AuthException.class.getName() + ": " + e.getMessage();
logStacktrace = false;
} else if (e instanceof BadRequestException || rootCause instanceof BadRequestException) {
errorCode = HttpURLConnection.HTTP_BAD_REQUEST;
response = e.getMessage();
} else if (e instanceof PathNotFoundException || rootCause instanceof PathNotFoundException) {
errorCode = HttpURLConnection.HTTP_NOT_FOUND;
response = e.getMessage();
} else if (e instanceof TimeoutException || rootCause instanceof TimeoutException) {
errorCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
if (e.getMessage().contains("Checking took longer than")) {
response = e.getMessage(); // more specific information already provided
} else {
response = "Checking took longer than " + config.getMaxCheckTimeMillisAnonymous() / 1000.0f + " seconds, which is this server's limit. Please make sure you have selected the proper language or consider submitting a shorter text.";
}
} else if (e instanceof UnavailableException) {
errorCode = HTTP_UNAVAILABLE;
response = e.getMessage();
} else {
response = "Internal Error: " + e.getMessage();
errorCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
textLoggingAllowed = true;
}
long endTime = System.currentTimeMillis();
logError(remoteAddress, e, errorCode, httpExchange, parameters, textLoggingAllowed, logStacktrace, endTime-startTime);
sendError(httpExchange, errorCode, "Error: " + response);
globalSpan.recordException(e);
globalSpan.setStatus(StatusCode.ERROR);
} finally {
logger.info("Handled request in {}ms; sending code {}", System.currentTimeMillis() - startTime, httpExchange.getResponseCode());
httpExchange.close();
mdcRequestID.close();
globalSpan.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpExchange.getResponseCode());
globalSpan.end();
if (incrementHandleCount) {
reqCounter.decrementHandleCount(reqId);
}
}
}