Skip to content

feat(http): add POST request support - #7676

Merged
DennisOSRM merged 9 commits into
masterfrom
dlx/post
Aug 13, 2026
Merged

feat(http): add POST request support#7676
DennisOSRM merged 9 commits into
masterfrom
dlx/post

Conversation

@DennisOSRM

@DennisOSRM DennisOSRM commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

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:

  • include/server/api/json_parameters_parser.hpp -- header for the JSON parameter parser
  • src/server/api/json_parameters_parser.cpp -- implementation, ~570 lines
  • unit_tests/server/json_parameters_parser.cpp -- unit tests

Key changes in existing files:

  • src/server/request_handler.cpp -- dispatches to the JSON parser for POST, URL parser for GET
  • src/server/service/route_service.cpp and table_service.cpp -- accept either parameter type
  • src/server/connection.cpp -- reads the POST body up to a configurable maximum size
  • include/server/header_size.hpp -- bumped the maximum header/body size
  • src/tools/routed.cpp -- wired up the new parser
  • docs/http.md -- documented the JSON request format
  • features/support/http.js and route.js -- updated Cucumber test helpers

Cleanup along the way:

  • Removed two unused headers (postprocessing_toolkit.hpp, name_table.hpp)
  • Added missing #include guards in a few engine headers

implements #7660

Copilot AI lite review requested due to automatic review settings August 5, 2026 17:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 route and table, 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.

Comment on lines +49 to +53
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;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Replaced ::tolower with a lambda that casts to unsigned char first.

Comment thread include/server/request_handler.hpp
Comment thread src/server/request_handler.cpp
Comment thread src/server/request_handler.cpp Outdated
Comment on lines +185 to +189
// 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());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Both log lines now check LogPolicy::GetInstance().IsMute() before calling CompactJsonForLog.

Comment on lines +57 to +63
// 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Added #include <cstddef>.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.77%. Comparing base (ad6a8cb) to head (0d07cb7).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@DennisOSRM
DennisOSRM merged commit 568bb35 into master Aug 13, 2026
27 of 43 checks passed
@DennisOSRM
DennisOSRM deleted the dlx/post branch August 13, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP POST support

2 participants