LCOV - code coverage report
Current view: top level - http/server - body_decoders.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 3 3
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 2 2

            Line data    Source code
       1              : #ifndef THINGER_HTTP_SERVER_BODY_DECODERS_HPP
       2              : #define THINGER_HTTP_SERVER_BODY_DECODERS_HPP
       3              : 
       4              : #include "../common/http_response.hpp"
       5              : #include "response_compression.hpp"     // Accept-Encoding negotiation, declared here before
       6              : #include <nlohmann/json.hpp>
       7              : #include <cstddef>
       8              : #include <functional>
       9              : #include <optional>
      10              : #include <stdexcept>
      11              : #include <string>
      12              : #include <string_view>
      13              : #include <utility>
      14              : #include <vector>
      15              : 
      16              : namespace thinger::http {
      17              : 
      18              : // Media type of a Content-Type (or Accept) value: "type/subtype" and its parameters
      19              : // (RFC 9110, 8.3.1), e.g. "Application/JSON; charset=\"UTF-8\"" -> "application/json",
      20              : // {"charset", "UTF-8"}
      21              : struct media_type {
      22              :     // "type/subtype", lowercase
      23              :     std::string type;
      24              :     // Parameters in order: names lowercase, values unquoted (case kept)
      25              :     std::vector<std::pair<std::string, std::string>> parameters;
      26              : 
      27              :     // Parse a media type; nullopt if it is malformed. Media ranges ("*/*", "text/*") are
      28              :     // accepted too, for Accept headers.
      29              :     static std::optional<media_type> parse(std::string_view value);
      30              : 
      31              :     // Value of a parameter (the first one with that name), empty if missing
      32              :     std::string parameter(std::string_view name) const;
      33              : };
      34              : 
      35              : // Limits applied while decoding a body, against inputs that are small but expensive to
      36              : // decode or to use. The body size itself is limited by the server (set_max_body_size).
      37              : // Besides these, a body never decodes to more values than it has bytes (every value takes
      38              : // at least one byte in JSON, MessagePack and CBOR; UBJSON can declare arrays of values
      39              : // without payload, such as a billion nulls in a few bytes, which are rejected).
      40              : struct body_limits {
      41              :     // Maximum nesting of arrays and objects (a scalar is at depth 0, [] or {} at depth 1).
      42              :     // Decoding is iterative, but the decoded value is often used recursively (schema
      43              :     // validation, dump(), copies, comparisons), so an unlimited depth could overflow the stack.
      44              :     size_t max_depth = 128;
      45              : };
      46              : 
      47              : // A body that cannot be decoded: 400 Bad Request if it is invalid, or 415 Unsupported Media
      48              : // Type if its type (or charset) is not supported. The message is sent to the client.
      49              : class body_decode_error : public std::runtime_error {
      50              : public:
      51          326 :     explicit body_decode_error(const std::string& message,
      52              :                                http_response::status status = http_response::status::bad_request)
      53          326 :         : std::runtime_error(message), status_(status) {}
      54              : 
      55          320 :     http_response::status status() const { return status_; }
      56              : 
      57              : private:
      58              :     http_response::status status_;
      59              : };
      60              : 
      61              : // Decodes a body of a media type into a JSON value, within the limits. Throws
      62              : // body_decode_error if it cannot; any other exception is answered as an invalid body (400).
      63              : using body_decoder = std::function<nlohmann::json(std::string_view body, const media_type& type,
      64              :                                                   const body_limits& limits)>;
      65              : 
      66              : // A body format: a decoder and the media types it decodes
      67              : struct body_format {
      68              :     // Name the routes accept it by, e.g. "msgpack"
      69              :     std::string name;
      70              :     // Media types it decodes (lowercase), e.g. "application/msgpack", "application/x-msgpack"
      71              :     std::vector<std::string> media_types;
      72              :     // Structured syntax suffix it also decodes, e.g. "+json" for "application/problem+json"
      73              :     // (empty if none)
      74              :     std::string suffix;
      75              :     body_decoder decode;
      76              : };
      77              : 
      78              : // Decoders of request bodies by media type, used for the routes taking a decoded body
      79              : // (see route::accepts). Built in, each one removable:
      80              : //
      81              : //   name      media types                                    decoded as
      82              : //   json      application/json (and any +json)               the JSON value
      83              : //   msgpack   application/msgpack, application/x-msgpack     the value (bin/ext as binary)
      84              : //   cbor      application/cbor (and any +cbor)               the value (byte strings as binary,
      85              : //                                                            tagged items rejected)
      86              : //   ubjson    application/ubjson                             the value
      87              : //   form      application/x-www-form-urlencoded              an object of strings (repeated
      88              : //                                                            names: an array of strings)
      89              : //   multipart multipart/form-data                            the same, with files as objects
      90              : //                                                            {filename, content_type, content}
      91              : //   text      text/plain                                     a string (valid UTF-8)
      92              : //   binary    application/octet-stream                       a binary value (json::binary)
      93              : //
      94              : // JSON, form and text take no charset other than UTF-8 (415); strings in every format must
      95              : // be valid UTF-8 (400 otherwise), so a decoded value can always be dumped. Configure it
      96              : // before serving: it is read concurrently by the worker threads.
      97              : class body_decoder_registry {
      98              : public:
      99              :     // With the built-in decoders
     100              :     body_decoder_registry();
     101              : 
     102              :     // Add a decoder, replacing the one with the same name (if any); its media types (and
     103              :     // suffix) are removed from any other decoder
     104              :     body_decoder_registry& add(const std::string& name, std::vector<std::string> media_types,
     105              :                                body_decoder decoder, std::string suffix = {});
     106              : 
     107              :     // Remove a decoder (e.g. a built-in one): bodies of its media types are no longer
     108              :     // decoded, so routes accepting them answer 415. Returns false if there was none.
     109              :     bool remove(const std::string& name);
     110              : 
     111              :     // Decoder by name, null if none
     112              :     const body_format* find(std::string_view name) const;
     113              : 
     114              :     // Decoder for a media type ("type/subtype", lowercase): by its media types, or else by
     115              :     // its suffix; null if none
     116              :     const body_format* find_media_type(std::string_view type) const;
     117              : 
     118              :     // A media type exactly as a decoder lists it, without parameters (as most clients send
     119              :     // their Content-Type, e.g. "application/json"): that media type, parsed when the decoder
     120              :     // was added, and its decoder; {null, null} if it is not one. A Content-Type found here
     121              :     // needs no parsing on each request.
     122              :     std::pair<const media_type*, const body_format*> find_plain_media_type(std::string_view type) const;
     123              : 
     124              :     const std::vector<body_format>& formats() const { return formats_; }
     125              : 
     126              :     // The built-in decoders, to reuse in custom ones
     127              :     static nlohmann::json decode_json(std::string_view body, const media_type& type, const body_limits& limits);
     128              :     static nlohmann::json decode_msgpack(std::string_view body, const media_type& type, const body_limits& limits);
     129              :     static nlohmann::json decode_cbor(std::string_view body, const media_type& type, const body_limits& limits);
     130              :     static nlohmann::json decode_ubjson(std::string_view body, const media_type& type, const body_limits& limits);
     131              :     static nlohmann::json decode_form(std::string_view body, const media_type& type, const body_limits& limits);
     132              :     static nlohmann::json decode_multipart(std::string_view body, const media_type& type, const body_limits& limits);
     133              :     static nlohmann::json decode_text(std::string_view body, const media_type& type, const body_limits& limits);
     134              :     static nlohmann::json decode_binary(std::string_view body, const media_type& type, const body_limits& limits);
     135              : 
     136              : private:
     137              :     // Index the plain media types of the decoders (after adding or removing one)
     138              :     void index_plain_media_types();
     139              : 
     140              :     std::vector<body_format> formats_;
     141              :     // The media types of the decoders that are plain (lowercase "type/subtype"), parsed,
     142              :     // with the index of their decoder in formats_
     143              :     std::vector<std::pair<media_type, size_t>> plain_media_types_;
     144              : };
     145              : 
     146              : // How the bodies of the routes are decoded: the application settings (see
     147              : // http_application::body_decoders), shared by its virtual hosts
     148              : struct body_settings {
     149              :     body_decoder_registry decoders;
     150              :     // Types accepted by routes taking a decoded body that do not set their own (route::accepts)
     151              :     std::vector<std::string> accepted{"json"};
     152              :     body_limits limits;
     153              : 
     154              :     // Defaults, for routes handled without an application
     155              :     static const body_settings& defaults();
     156              : };
     157              : 
     158              : // Media type assumed for a body without Content-Type: JSON, as before body decoders existed
     159              : inline constexpr std::string_view default_body_media_type = "application/json";
     160              : 
     161              : // Result of checking the Content-Type of a body against the types a route accepts
     162              : struct body_type_match {
     163              :     // Whether the body is accepted; if not, `error` tells why (for a 415 answer)
     164              :     bool accepted = false;
     165              :     std::string error;
     166              :     // Media type of the body (default_body_media_type without Content-Type)
     167              :     media_type type;
     168              :     // Decoder for it, null if none is registered (a type accepted only by a route reading
     169              :     // the body itself)
     170              :     const body_format* format = nullptr;
     171              : };
     172              : 
     173              : // Check a Content-Type header value (empty if missing) against the accepted types: names of
     174              : // decoders in the registry, or media types. A type is accepted if its media type is listed,
     175              : // or the name of its decoder.
     176              : body_type_match match_body_type(const body_decoder_registry& decoders,
     177              :                                 const std::vector<std::string>& accepted,
     178              :                                 std::string_view content_type);
     179              : 
     180              : // Media types of a list of accepted types (the media types of each decoder named, and the
     181              : // media types listed), in order and without repetitions, e.g. for API documentation
     182              : std::vector<std::string> accepted_media_types(const body_decoder_registry& decoders,
     183              :                                               const std::vector<std::string>& accepted);
     184              : 
     185              : // Response encoding chosen by the Accept header of a request, among JSON, MessagePack, CBOR
     186              : // and UBJSON (see response::encode)
     187              : struct response_encoding {
     188              :     std::string media_type;     // Content-Type to send, e.g. "application/msgpack"
     189              :     std::string format;         // "json", "msgpack", "cbor" or "ubjson"
     190              : };
     191              : 
     192              : // The encoding the Accept header (empty if missing) prefers, by quality and then in the
     193              : // order JSON, MessagePack, CBOR, UBJSON; JSON if it accepts none of them
     194              : response_encoding negotiate_response_encoding(std::string_view accept);
     195              : 
     196              : // Encode a value in a format of negotiate_response_encoding()
     197              : std::string encode_body(const nlohmann::json& value, const std::string& format);
     198              : 
     199              : } // namespace thinger::http
     200              : 
     201              : #endif // THINGER_HTTP_SERVER_BODY_DECODERS_HPP
        

Generated by: LCOV version 2.0-1