From 9e4a3ffcff4e612644856654262a0164a53b2bfd Mon Sep 17 00:00:00 2001 From: pcace Date: Thu, 16 Apr 2026 20:17:52 +0200 Subject: [PATCH] Support large routing requests via POST and PUT --- .../main/java/btools/server/RouteServer.java | 112 ++++++++++++++++-- .../btools/server/request/ServerHandler.java | 10 +- .../java/btools/server/RouteServerTest.java | 54 +++++++++ docs/developers/http_server.md | 41 +++++++ misc/scripts/standalone/server.sh | 16 ++- 5 files changed, 221 insertions(+), 12 deletions(-) diff --git a/brouter-server/src/main/java/btools/server/RouteServer.java b/brouter-server/src/main/java/btools/server/RouteServer.java index 2def82bf8..37ad5bdc7 100644 --- a/brouter-server/src/main/java/btools/server/RouteServer.java +++ b/brouter-server/src/main/java/btools/server/RouteServer.java @@ -38,6 +38,8 @@ public class RouteServer extends Thread implements Comparable { public static final String PROFILE_UPLOAD_URL = "/brouter/profile"; + public static final String PROFILES_URL = "/brouter/getprofiles"; + public static final String ROUTING_URL = "/brouter"; static final String HTTP_STATUS_OK = "200 OK"; static final String HTTP_STATUS_BAD_REQUEST = "400 Bad Request"; static final String HTTP_STATUS_FORBIDDEN = "403 Forbidden"; @@ -83,6 +85,8 @@ public void run() { String agent = null; String encodings = null; String xff = null; // X-Forwarded-For + String contentType = null; + int contentLength = -1; // more headers until first empty line for (; ; ) { @@ -99,15 +103,21 @@ public void run() { if (getline == null) { getline = line; } - line = line.toLowerCase(); - if (line.startsWith("user-agent: ")) { - agent = line.substring("user-agent: ".length()); + String lowerLine = line.toLowerCase(Locale.ROOT); + if (lowerLine.startsWith("user-agent: ")) { + agent = lowerLine.substring("user-agent: ".length()); } - if (line.startsWith("accept-encoding: ")) { - encodings = line.substring("accept-encoding: ".length()); + if (lowerLine.startsWith("accept-encoding: ")) { + encodings = lowerLine.substring("accept-encoding: ".length()); } - if (line.startsWith("x-forwarded-for: ")) { - xff = line.substring("x-forwarded-for: ".length()); + if (lowerLine.startsWith("x-forwarded-for: ")) { + xff = lowerLine.substring("x-forwarded-for: ".length()); + } + if (lowerLine.startsWith("content-type: ")) { + contentType = line.substring("content-type: ".length()).trim(); + } + if (lowerLine.startsWith("content-length: ")) { + contentLength = Integer.parseInt(lowerLine.substring("content-length: ".length()).trim()); } } @@ -147,10 +157,39 @@ public void run() { return; } - String url = getline.split(" ")[1]; + String[] requestLineParts = getline.split(" "); + if (requestLineParts.length < 2) { + writeHttpHeader(bw, HTTP_STATUS_BAD_REQUEST); + bw.write("Malformed HTTP request line\n"); + bw.flush(); + return; + } + + String method = requestLineParts[0]; + String url = requestLineParts[1]; + String path = getUrlPath(url); + + if (ROUTING_URL.equals(path) && "OPTIONS".equals(method)) { + String corsHeaders = "Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS\r\n" + + "Access-Control-Allow-Headers: Content-Type\r\n"; + writeHttpHeader(bw, "text/plain", null, corsHeaders, HTTP_STATUS_OK); + bw.flush(); + return; + } RoutingParamCollector routingParamCollector = new RoutingParamCollector(); Map params = routingParamCollector.getUrlParams(url); + if (ROUTING_URL.equals(path) && isBodyRequest(method)) { + try { + params.putAll(readRoutingRequestParams(br, routingParamCollector, contentType, contentLength)); + } catch (IllegalArgumentException e) { + writeHttpHeader(bw, HTTP_STATUS_BAD_REQUEST); + bw.write(e.getMessage()); + bw.write("\n"); + bw.flush(); + return; + } + } long maxRunningTime = getMaxRunningTime(); @@ -384,6 +423,54 @@ private static Map getUrlParams(String url) throws UnsupportedEn return params; } + private static String getUrlPath(String url) { + int queryIdx = url.indexOf('?'); + return queryIdx < 0 ? url : url.substring(0, queryIdx); + } + + private static boolean isBodyRequest(String method) { + return "POST".equals(method) || "PUT".equals(method); + } + + private static Map readRoutingRequestParams( + BufferedReader br, + RoutingParamCollector routingParamCollector, + String contentType, + int contentLength) throws IOException { + Map params = new HashMap<>(); + if (contentLength == 0) { + return params; + } + if (contentLength < 0) { + return params; + } + + if (contentType != null) { + String lowerContentType = contentType.toLowerCase(Locale.ROOT); + if (!lowerContentType.startsWith("application/x-www-form-urlencoded") + && !lowerContentType.startsWith("text/plain")) { + throw new IllegalArgumentException("Unsupported Content-Type for routing request: " + contentType); + } + } + + int maxRequestLength = getMaxRequestLength(); + if (contentLength > maxRequestLength) { + throw new IllegalArgumentException("Routing request body too large (" + contentLength + " > " + maxRequestLength + ")"); + } + + char[] requestBody = new char[contentLength]; + int offset = 0; + while (offset < contentLength) { + int read = br.read(requestBody, offset, contentLength - offset); + if (read < 0) { + throw new IOException("Unexpected end of routing request body"); + } + offset += read; + } + params.putAll(routingParamCollector.getUrlParams(new String(requestBody))); + return params; + } + private static long getMaxRunningTime() { long maxRunningTime = 60000; String sMaxRunningTime = System.getProperty("maxRunningTime"); @@ -393,6 +480,15 @@ private static long getMaxRunningTime() { return maxRunningTime; } + private static int getMaxRequestLength() { + int maxRequestLength = 1000000; + String sMaxRequestLength = System.getProperty("maxRequestLength"); + if (sMaxRequestLength != null) { + maxRequestLength = Integer.parseInt(sMaxRequestLength); + } + return maxRequestLength; + } + private static void writeHttpHeader(BufferedWriter bw, String status) throws IOException { writeHttpHeader(bw, "text/plain", status); } diff --git a/brouter-server/src/main/java/btools/server/request/ServerHandler.java b/brouter-server/src/main/java/btools/server/request/ServerHandler.java index fcb536c92..7d866e996 100644 --- a/brouter-server/src/main/java/btools/server/request/ServerHandler.java +++ b/brouter-server/src/main/java/btools/server/request/ServerHandler.java @@ -12,9 +12,13 @@ import btools.server.ServiceContext; /** - * URL query parameter handler for web and standalone server. Supports all + * Routing parameter handler for web and standalone server. Supports all * BRouter features without restrictions. *

+ * Parameters can either be sent via the request URL (GET) or via a request + * body using {@code application/x-www-form-urlencoded} or {@code text/plain} + * (POST/PUT). + *

* Parameters: *

* lonlats = lon,lat|... (unlimited list of lon,lat waypoints separated by |) @@ -35,6 +39,10 @@ * Example URLs: * {@code http://localhost:17777/brouter?lonlats=8.799297,49.565883|8.811764,49.563606&nogos=&profile=trekking&alternativeidx=0&format=gpx} * {@code http://localhost:17777/brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml&trackname=Ride&pois=1.1,2.1,Barner Bar} + * {@code curl -X POST http://localhost:17777/brouter -H 'Content-Type: application/x-www-form-urlencoded' --data 'lonlats=8.799297,49.565883%7C8.811764,49.563606&profile=trekking&format=gpx'} + * {@code curl -X PUT http://localhost:17777/brouter -H 'Content-Type: application/x-www-form-urlencoded' --data-binary @request-body.txt} + * {@code http://localhost:17777/brouter/getprofiles} + * {@code http://localhost:17777/brouter/getprofiles/trekking.brf} */ public class ServerHandler extends RequestHandler { diff --git a/brouter-server/src/test/java/btools/server/RouteServerTest.java b/brouter-server/src/test/java/btools/server/RouteServerTest.java index 08f9bf319..ae28e2f52 100644 --- a/brouter-server/src/test/java/btools/server/RouteServerTest.java +++ b/brouter-server/src/test/java/btools/server/RouteServerTest.java @@ -83,6 +83,46 @@ public void defaultRouteTrekking() throws IOException, URISyntaxException { Assert.assertEquals("4", geoJson.query("/features/0/properties/filtered ascend")); } + @Test + public void routeRequestFromPostBody() throws IOException, URISyntaxException { + URL requestUrl = new URI(baseUrl + "brouter").toURL(); + HttpURLConnection httpConnection = (HttpURLConnection) requestUrl.openConnection(); + + httpConnection.setRequestMethod("POST"); + httpConnection.setDoOutput(true); + httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + String requestBody = "lonlats=8.723037,50.000491%7C8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson"; + try (OutputStream outputStream = httpConnection.getOutputStream()) { + outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8)); + } + + Assert.assertEquals(HttpURLConnection.HTTP_OK, httpConnection.getResponseCode()); + + InputStream inputStream = httpConnection.getInputStream(); + JSONObject geoJson = new JSONObject(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + Assert.assertEquals("1169", geoJson.query("/features/0/properties/track-length")); + } + + @Test + public void largePolygonRequestFromPutBody() throws IOException, URISyntaxException { + URL requestUrl = new URI(baseUrl + "brouter").toURL(); + HttpURLConnection httpConnection = (HttpURLConnection) requestUrl.openConnection(); + + httpConnection.setRequestMethod("PUT"); + httpConnection.setDoOutput(true); + httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + String requestBody = "lonlats=8.723037,50.000491%7C8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson&polygons=" + buildLargePolygon(); + try (OutputStream outputStream = httpConnection.getOutputStream()) { + outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8)); + } + + Assert.assertEquals(HttpURLConnection.HTTP_OK, httpConnection.getResponseCode()); + + InputStream inputStream = httpConnection.getInputStream(); + JSONObject geoJson = new JSONObject(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + Assert.assertEquals("1169", geoJson.query("/features/0/properties/track-length")); + } + @Test public void overrideParameter() throws IOException, URISyntaxException { URL requestUrl = new URI(baseUrl + "brouter?lonlats=8.723037,50.000491%7C8.712737,50.002899&nogos=&profile=trekking&alternativeidx=0&format=geojson&profile:avoid_unsafe=1").toURL(); @@ -248,4 +288,18 @@ public void invalidUrl() throws IOException, URISyntaxException { Assert.assertEquals(HttpURLConnection.HTTP_NOT_FOUND, httpConnection.getResponseCode()); } + + private static String buildLargePolygon() { + StringBuilder polygon = new StringBuilder(); + for (int i = 0; i < 600; i++) { + double angle = Math.PI * 2. * i / 600.; + double lon = 8.80 + Math.cos(angle) * 0.01; + double lat = 50.05 + Math.sin(angle) * 0.01; + if (i > 0) { + polygon.append(','); + } + polygon.append(lon).append(',').append(lat); + } + return polygon.toString(); + } } diff --git a/docs/developers/http_server.md b/docs/developers/http_server.md index 2b6df157a..e9ad835c6 100644 --- a/docs/developers/http_server.md +++ b/docs/developers/http_server.md @@ -15,4 +15,45 @@ BRouter HTTP server for various platforms. The API endpoints exposed by this HTTP server are documented in the `ServerHandler.java` +Routing requests to `/brouter` can use either: + +* `GET` with the existing URL query parameters +* `POST` or `PUT` with the same parameter string in the request body + +For request bodies, use `application/x-www-form-urlencoded` (preferred) or +`text/plain`. This is useful for large `nogos`, `polylines` or `polygons` +payloads that would otherwise hit browser, proxy or server URL-length limits. + +The standalone startup script now defaults to request bodies slightly above +5 MiB: + +* `BROUTER_MAX_REQUEST_LENGTH=6291456` +* `BROUTER_JAVA_XMX=256M` +* `BROUTER_JAVA_XMS=256M` +* `BROUTER_JAVA_XMN=16M` + +You can override those values via environment variables, for example: + +```sh +BROUTER_MAX_REQUEST_LENGTH=8388608 BROUTER_JAVA_XMX=512M ./misc/scripts/standalone/server.sh +``` + +Containerized deployments can pass the same environment variables through +their startup wrapper before invoking `misc/scripts/standalone/server.sh`. + +For large polygon uploads it is usually easier to put the full parameter string +into a file and send it with `PUT`: + +```sh +curl -X PUT http://localhost:17777/brouter \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ + --data-binary @request-body.txt +``` + +Example `request-body.txt` content: + +```text +lonlats=8.723037,50.000491|8.712737,50.002899&profile=trekking&alternativeidx=0&format=geojson&polygons=8.81,50.05,8.8101,50.0501,8.8102,50.0502 +``` + Please see also [IBRouterService.aidl](./android_service.md) for calling parameter. diff --git a/misc/scripts/standalone/server.sh b/misc/scripts/standalone/server.sh index 92d85f052..dbf5e3b43 100755 --- a/misc/scripts/standalone/server.sh +++ b/misc/scripts/standalone/server.sh @@ -4,8 +4,18 @@ cd "$(dirname "$0")" # BRouter standalone server # java -cp brouter.jar btools.brouter.RouteServer [bindaddress] -# maxRunningTime is the request timeout in seconds, set to 0 to disable timeout -JAVA_OPTS="-Xmx128M -Xms128M -Xmn8M -DmaxRunningTime=300 -DuseRFCMimeType=false" +# maxRunningTime is the request timeout in seconds, set to 0 to disable timeout. +# maxRequestLength is the maximum accepted request body size in bytes. The default +# is sized to allow PUT/POST bodies slightly above 5 MiB. +BROUTER_JAVA_XMX=${BROUTER_JAVA_XMX:-"256M"} +BROUTER_JAVA_XMS=${BROUTER_JAVA_XMS:-$BROUTER_JAVA_XMX} +BROUTER_JAVA_XMN=${BROUTER_JAVA_XMN:-"16M"} +BROUTER_MAX_RUNNING_TIME=${BROUTER_MAX_RUNNING_TIME:-"300"} +BROUTER_MAX_REQUEST_LENGTH=${BROUTER_MAX_REQUEST_LENGTH:-"6291456"} +BROUTER_USE_RFC_MIME_TYPE=${BROUTER_USE_RFC_MIME_TYPE:-"false"} + +DEFAULT_JAVA_OPTS="-Xmx$BROUTER_JAVA_XMX -Xms$BROUTER_JAVA_XMS -Xmn$BROUTER_JAVA_XMN -DmaxRunningTime=$BROUTER_MAX_RUNNING_TIME -DmaxRequestLength=$BROUTER_MAX_REQUEST_LENGTH -DuseRFCMimeType=$BROUTER_USE_RFC_MIME_TYPE" +JAVA_OPTS=${JAVA_OPTS:-$DEFAULT_JAVA_OPTS} # If paths are unset, first search in locations matching the directory structure # as found in the official BRouter zip archive @@ -28,4 +38,4 @@ if [ ! -e "$CUSTOMPROFILESPATH" ]; then CUSTOMPROFILESPATH="../customprofiles" fi -java $JAVA_OPTS -cp $CLASSPATH btools.server.RouteServer "$SEGMENTSPATH" "$PROFILESPATH" "$CUSTOMPROFILESPATH" 17777 1 $BINDADDRESS +exec java $JAVA_OPTS -cp $CLASSPATH btools.server.RouteServer "$SEGMENTSPATH" "$PROFILESPATH" "$CUSTOMPROFILESPATH" 17777 1 $BINDADDRESS