feat(http): add POST request support - #7676
Conversation
There was a problem hiding this comment.
Pull request overview
Adds HTTP POST support (JSON request bodies) for OSRM’s HTTP API, implementing the request discussed in #7660. This extends the existing Boost.Beast server so route and table can accept coordinates/options in the body to avoid URL/header size limits, while keeping GET behavior intact.
Changes:
- Added a JSON-body parameter parser and POST execution path for
routeandtable, including request-body size limiting. - Updated request handling for method routing (GET/HEAD/POST/OPTIONS), CORS headers, and logging.
- Added unit + cucumber coverage to validate JSON parsing and GET/POST equivalence, plus updated HTTP docs.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| unit_tests/server/json_parameters_parser.cpp | New unit tests for JSON parameter parsing and parity with URL parsing. |
| src/tools/routed.cpp | Adds --max-request-body-size and plumbs body size into server creation/logging. |
| src/server/service/table_service.cpp | Adds RunJSONQuery for table and shares GET/POST execution via runTable. |
| src/server/service/route_service.cpp | Adds RunJSONQuery for route and shares GET/POST execution via runRoute. |
| src/server/service_handler.cpp | Refactors service resolution and adds JSON-body query dispatch. |
| src/server/request_handler.cpp | Implements method handling (OPTIONS/POST), content-type checks, and POST routing. |
| src/server/connection.cpp | Sets Beast parser body_limit() based on configured max request body size. |
| src/server/api/url_parser.cpp | Adds parseURLPrefix for POST endpoints without query-in-URL. |
| src/server/api/json_parameters_parser.cpp | New JSON-to-parameters parser for Route/Table (RapidJSON-based). |
| include/server/service/table_service.hpp | Declares RunJSONQuery override for table. |
| include/server/service/route_service.hpp | Declares RunJSONQuery override for route. |
| include/server/service/base_service.hpp | Adds default RunJSONQuery returning NotImplemented. |
| include/server/service_handler.hpp | Extends handler interface with POST/JSON overload. |
| include/server/server.hpp | Plumbs max_body_size through Server::CreateServer and constructor. |
| include/server/request_handler.hpp | Adds shared CORS header helper used by responses/errors. |
| include/server/header_size.hpp | Adds deriveMaxBodySize() to size POST body limits from config. |
| include/server/connection.hpp | Stores and passes max_body_size_ to the HTTP parser. |
| include/server/api/url_parser.hpp | Declares parseURLPrefix helpers. |
| include/server/api/json_parameters_parser.hpp | New public interface for JSON parameter parsing. |
| include/extractor/name_table.hpp | Removes deprecated forwarding header. |
| include/engine/hint.hpp | Adds defaulted equality for Hint to enable value comparisons in tests. |
| include/engine/guidance/postprocessing_toolkit.hpp | Removes deprecated/unused header. |
| include/engine/api/table_parameters.hpp | Adds defaulted equality for TableParameters. |
| include/engine/api/route_parameters.hpp | Adds defaulted equality for RouteParameters. |
| include/engine/api/base_parameters.hpp | Adds defaulted equality for BaseParameters. |
| features/support/route.js | Adds GET/POST equivalence checks for route/table in cucumber tests. |
| features/support/http.js | Adds a POST-capable HTTP helper for cucumber tests. |
| docs/http.md | Documents POST JSON body API for route/table and body-size option. |
Suppressed comments (1)
src/server/request_handler.cpp:270
- The access log line also unconditionally evaluates
is_post ? " " + CompactJsonForLog(current_request.body()) : ...even when logging is muted, because the concatenation happens before util::Log() can short-circuit. Wrapping the access-log emission with a LogPolicy check avoids parsing/compacting potentially large bodies when nothing will be logged.
<< request_string
// POST: append the JSON body (compacted to one line) so the request
// can be replayed from the log alone.
<< (is_post ? " " + CompactJsonForLog(current_request.body())
: std::string());
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bool IsJsonContentType(std::string content_type) | ||
| { | ||
| std::transform(content_type.begin(), content_type.end(), content_type.begin(), ::tolower); | ||
| return content_type.rfind("application/json", 0) == 0; | ||
| } |
There was a problem hiding this comment.
Fixed. Replaced ::tolower with a lambda that casts to unsigned char first.
| // Echo every incoming request to the console as a single line. GET carries its full query | ||
| // in the URL; POST additionally gets its JSON body appended (compacted to one line) so both | ||
| // request types are logged identically and can be replayed from the log alone. | ||
| util::Log() << "[req][" << tid << "] " << request_string | ||
| << (is_post ? " " + CompactJsonForLog(current_request.body()) : std::string()); |
There was a problem hiding this comment.
Fixed. Both log lines now check LogPolicy::GetInstance().IsMute() before calling CompactJsonForLog.
| // Estimates the maximum HTTP request body size (for POST requests with a JSON body). | ||
| // A JSON coordinate such as `[13.388000,52.517000],` is ~24 bytes; per-coordinate options | ||
| // (hints, bearings, ...) add more, so we budget generously per coordinate on top of a | ||
| // fixed floor to accommodate the surrounding JSON structure and options. | ||
| inline std::size_t deriveMaxBodySize(const engine::EngineConfig &config) | ||
| { | ||
| constexpr std::size_t MIN_BODY_SIZE = 1024 * 1024; // 1 MiB |
There was a problem hiding this comment.
Good catch. Added #include <cstddef>.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #7676 +/- ##
==========================================
- Coverage 94.47% 91.77% -2.70%
==========================================
Files 516 519 +3
Lines 40418 41380 +962
==========================================
- Hits 38185 37978 -207
- Misses 2233 3402 +1169 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Extend the JSON POST API from route and table to the match service. The route option block in the JSON parser moves into a shared parseRouteParameters() helper, mirroring how the URL grammar composes route_grammar::route_options into the match root rule. On top of it the new MatchParameters specialization parses timestamps, gaps and tidy. MatchService gains a RunJSONQuery override; the validation and dispatch tail is hoisted into runMatch() so the GET and POST paths share it, as RouteService already does. MatchParameters gets a defaulted operator== so that comparing two match requests does not silently fall back to the RouteParameters base and ignore the match specific members. Cucumber issues every match scenario as both a GET and a POST and asserts the responses are equivalent.
Hints are deprecated: the engine generates placeholder segment hints for responses and never uses an incoming hint to look up a phantom node. The JSON POST parser therefore only validates the shape of the hints array and records one empty entry per element, so that the size check in IsValid() still sees the same counts as the URL API. With the base64 decoding gone, nothing inside the three parseJSONParameters specializations can throw, so their try/catch wrappers are removed as well.
Unit tests for the pieces that can be driven without a routing engine: - every rejection path of the JSON parameter parser, plus the option values that were only reachable through a POST body (polyline6 coordinates, per-coordinate nulls, by_legs, gaps=split, boolean table annotations) - the request handler, against a stub service handler: CORS preflight, unsupported methods, the Content-Type check, a POST URL that carries a query, and the single-line access log for a body that is not JSON - parseURLPrefix, both the accepted forms and the two failure modes - deriveMaxBodySize A new post.feature covers what needs a running server: services without a POST override reporting NotImplemented, unknown service and version, bodies that fail to parse or to validate, and flatbuffers output for route, table and match.
Two issues found by fuzzing the parser with adversarial request bodies: 1. Stack overflow (remote DoS). RapidJSON's default parser recurses once per nesting level, so a body like "[[[[..." crashes the worker at a depth of ~150k characters -- comfortably inside the default request body-size limit, so a single unauthenticated POST takes the server down. Parse with kParseIterativeFlag, which uses an explicit heap stack and cannot be driven to recurse; RapidJSON also frees the resulting DOM without recursion. 2. Silent bearing truncation. Bearings were cast to short with a plain static_cast, wrapping an out-of-range value into an in-range one (65536 -> 0) that slips past Bearing::IsValid(). The URL grammar parses bearings with x3::short_ and rejects overflow; the JSON path now does the same by range-checking before the cast.
CompactJsonForLog ran on every POST, before the request was dispatched, and rendered the body by parsing it into a RapidJSON DOM and reserialising it. Both halves recurse once per nesting level: Document::Parse (default flags) and Value::Accept. A body such as "[[[[..." -- a few hundred KB, well inside the request body-size limit -- therefore overflowed the stack and killed the worker at the logging stage, ahead of the parser hardening in the previous commit and regardless of which service was addressed (even ones that do not support POST). Replace the DOM round-trip with a single non-recursive pass that strips insignificant whitespace and neutralises raw newlines while preserving string-literal contents, so the log line stays single-line and replayable at any nesting depth.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/server/request_handler.cpp:191
- The InvalidMethod response says "Use GET or POST", but the handler explicitly accepts HEAD (treated like GET) and OPTIONS (CORS preflight). This makes the error message inconsistent with the actual allowed methods.
json_result.values["message"] = "Method not allowed. Use GET or POST.";
src/server/request_handler.cpp:211
- The per-request "[req]" echo log was changed from logDEBUG to the default logINFO (util::Log()). This makes request echoing (and, for POST, potentially large bodies) appear at INFO level and duplicates the existing access log, increasing log volume and risk of leaking request data in normal deployments.
util::Log() << "[req][" << tid << "] " << request_string
- Echo the per-request "[req]" line at debug level again, as it was on the
GET-only path before POST support. The access log already records every
request (with the POST body) at info level, so emitting the echo at info
logged each request -- and each POST body -- twice in a normal deployment.
- Spell out all allowed methods in the 405 message ("Use GET, HEAD, POST,
or OPTIONS") so it matches the Allow header instead of naming only two.
Codecov flagged the two escape-tracking lines in CompactJsonForLog as the only uncovered lines in the diff: the existing tests logged a body with a space but no backslash escape, so the branch that keeps an escaped quote from ending a string literal early never ran. Add a case whose body carries an escaped quote and an escaped backslash inside a string and assert both, and the space that follows them, survive the compaction verbatim.
This PR adds HTTP POST support to the OSRM server (fixes #7660). Previously, clients had to encode all parameters into long GET URL query strings. This was awkward for complex requests with many coordinates, options, or hints, and it meant OSRM was the only major routing engine without POST support.
What changed:
The server now accepts JSON-encoded parameters in the POST body for the /route and /table endpoints. The existing URL-based GET flow is untouched; both methods work side by side.
A new json_parameters_parser reads JSON bodies and validates them against the same parameter schemas used by the URL parser. Both the route and table services were extended to accept either parameter source. The connection handler detects the HTTP method and routes accordingly.
New files:
Key changes in existing files:
Cleanup along the way:
implements #7660