LCOV - code coverage report
Current view: top level - http/server - response.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 % 1 1

            Line data    Source code
       1              : #ifndef THINGER_HTTP_SERVER_RESPONSE_HPP
       2              : #define THINGER_HTTP_SERVER_RESPONSE_HPP
       3              : 
       4              : #include "../common/http_response.hpp"
       5              : #include "../../util/logger.hpp"
       6              : #include "upgrade.hpp"
       7              : #include "error_format.hpp"
       8              : #include "../../util/compression.hpp"
       9              : #include <nlohmann/json.hpp>
      10              : #include <memory>
      11              : #include <functional>
      12              : #include <filesystem>
      13              : #include <chrono>
      14              : #include <optional>
      15              : #include <string>
      16              : #include <vector>
      17              : 
      18              : namespace thinger::http {
      19              : 
      20              : // Forward declarations: the connection types are not brought in (include their headers, or
      21              : // http_server.hpp, to use them)
      22              : class http_request;
      23              : class http_stream;
      24              : class server_connection;
      25              : class response_sink;
      26              : enum class body_framing;
      27              : class websocket_connection;
      28              : class sse_connection;
      29              : class http_application;
      30              : class virtual_host;
      31              : class response;
      32              : struct file_info;
      33              : 
      34              : namespace detail {
      35              :     struct cors_policy;
      36              :     struct cors_decision;
      37              : }
      38              : 
      39              : namespace file_server {
      40              :     struct representation;
      41              :     void send(response& res, const representation& file);
      42              : }
      43              : 
      44              : // Response to a request. It is a handle: copies share the response, so a handler may keep
      45              : // a copy and answer later, from any thread. Only the first answer is sent: the others are
      46              : // dropped, even when they run at the same time from several threads. Setting the status or
      47              : // headers before answering (status(), header()) is not thread-safe: do it from the thread
      48              : // running the handler, before any answer (a route timeout answers with the headers set so far).
      49              : // The answer sent takes them, moved into its message: once it started, they are gone.
      50              : // A response to a HEAD request is the one to GET without its body: every answer sends its
      51              : // headers, Content-Length included (of the body uncompressed: HEAD is never compressed), and
      52              : // drops the body; chunks and streamed data are dropped too, and SSE sends its headers alone.
      53              : class response {
      54              : public:
      55              :     response(const std::shared_ptr<server_connection>& connection,
      56              :              const std::shared_ptr<http_stream>& stream,
      57              :              const std::shared_ptr<http::http_request>& http_request);
      58              : 
      59              :     // Response written to `sink`, e.g. a memory_response for requests dispatched in memory
      60              :     response(std::shared_ptr<response_sink> sink,
      61              :              const std::shared_ptr<http::http_request>& http_request);
      62              : 
      63              :     // JSON response
      64              :     void json(const nlohmann::json& data, http::http_response::status status = http::http_response::status::ok);
      65              : 
      66              :     // The value as JSON, MessagePack, CBOR or UBJSON, as the Accept header of the request
      67              :     // prefers (by quality, then in that order); JSON if it accepts none of them or is missing.
      68              :     // The response has "Vary: Accept".
      69              :     void encode(const nlohmann::json& data, http::http_response::status status = http::http_response::status::ok);
      70              : 
      71              :     // Text response
      72              :     void send(const std::string& text, const std::string& content_type = "text/plain");
      73              : 
      74              :     // HTML response
      75            4 :     void html(const std::string& html) {
      76            4 :         send(html, "text/html");
      77            4 :     }
      78              : 
      79              :     // Error response, with the body produced by the error formatter (see
      80              :     // http_application::set_error_formatter): by default the message as text/plain, or a
      81              :     // JSON object for errors with details
      82              :     void error(http::http_response::status status, const std::string& message = "",
      83              :                const nlohmann::json& details = nullptr);
      84              : 
      85              :     // Formatter used by error() (set by the server)
      86              :     void set_error_formatter(std::shared_ptr<const error_formatter> formatter);
      87              : 
      88              :     // Set status code (for building custom responses: used by the answers that do not set
      89              :     // their own, such as send() and take_over(); not thread-safe)
      90              :     void status(http::http_response::status s);
      91              : 
      92              :     // Add a header to the response sent, whatever answer sends it (not thread-safe). The key
      93              :     // and value are moved into the response: pass an rvalue (e.g. std::move()) to save a copy.
      94              :     // An invalid header (a name that is not a token, or a value with CR, LF, NUL or another
      95              :     // control character but HTAB) is dropped, logged at debug level: the response is sent
      96              :     // without it
      97              :     void header(std::string key, std::string value);
      98              : 
      99              :     // Set a header to this value only, whatever was set before with its name (compared without
     100              :     // case), as setHeader() in Node and Header().Set() in Go; header() adds another field, as
     101              :     // appendHeader() and Header().Add(), which is what Set-Cookie needs. Same rules as header():
     102              :     // not thread-safe, and an invalid header is dropped, logged at debug level, leaving the
     103              :     // ones set before as they were
     104              :     void set_header(std::string key, std::string value);
     105              : 
     106              :     // Send raw http_response object (for advanced use cases). Its content is sent whole,
     107              :     // framed by a Content-Length of its size (any Content-Length or Transfer-Encoding it
     108              :     // carries is replaced, e.g. when copied from another response); 1xx, 204 and 304 are
     109              :     // sent without body, and 1xx and 204 without Content-Length. The headers set before
     110              :     // (header(), set_header()) are sent too, unless the message has its own of that name (as
     111              :     // the headers given to writeHead() in Node), but never Content-Length or Transfer-Encoding
     112              :     void send_response(const std::shared_ptr<http_response>& response);
     113              : 
     114              :     // WebSocket upgrade, with a message interface. With options.protocols, the first one the
     115              :     // client offers is accepted, and a client offering none of them is rejected with 400
     116              :     // (unless !protocol_required, then accepted without a subprotocol if it offers none); a
     117              :     // client offering only others, or any with no protocols, is rejected with 400. The
     118              :     // largest message and the keepalive are those of the options (16 MiB, ping after 60 s
     119              :     // without receiving anything, close after 120 s, by default). The handler is called on the
     120              :     // io_context of the connection, before the WebSocket starts reading (set its callbacks
     121              :     // there). A coroutine handler runs on that io_context too: the WebSocket starts reading
     122              :     // once it completes. Told of a shutdown through the connection (see
     123              :     // websocket_connection::on_shutdown()).
     124              :     void upgrade_websocket(const websocket_accept_options& options, websocket_handler handler);
     125              :     void upgrade_websocket(const websocket_accept_options& options, websocket_coroutine handler);
     126              : 
     127              :     // WebSocket upgrade handing out the WebSocket as a byte stream socket, for a protocol
     128              :     // running on it (MQTT, a custom one...) or a tunnel (socket_pipe): binary messages by
     129              :     // default, read as one continuous stream (see asio::websocket), with no keepalive unless
     130              :     // the options set one. The handler is called on the io_context of the socket once the
     131              :     // handshake response is written, with the upgraded_websocket: the WebSocket (holding any
     132              :     // frames the client sent right after its request), the subprotocol negotiated (see
     133              :     // websocket_accept_options) and its scope (see server_scope, e.g. to be told of a
     134              :     // shutdown). Rejected with 426 if the request is not a WebSocket upgrade, or 400 if its
     135              :     // key is missing or no subprotocol matches. The handler can be a coroutine, run on that
     136              :     // io_context.
     137              :     void upgrade_websocket_stream(const websocket_accept_options& options, websocket_stream_handler handler);
     138              :     void upgrade_websocket_stream(const websocket_accept_options& options, websocket_stream_coroutine handler);
     139              : 
     140              :     // Server-Sent Events (for a HEAD request, the headers alone: the handler is not called),
     141              :     // with the keep-alive of the options. The handler is called on the io_context of the
     142              :     // connection once the event stream started; a coroutine handler runs on that io_context.
     143              :     // Told of a shutdown through the connection (see sse_connection::on_shutdown()).
     144              :     void start_sse(sse_handler handler, const sse_options& options = {});
     145              :     void start_sse(sse_coroutine handler, const sse_options& options = {});
     146              : 
     147              :     // Take over the connection (e.g. for a custom protocol upgrade or a tunnel): sends
     148              :     // this response (status and headers set so far, 200 by default), then the server
     149              :     // stops handling the connection and passes it to `handler`: the socket, any bytes
     150              :     // already read past this request, and its scope (see server_scope, e.g. to be told of a
     151              :     // shutdown). The handler fully owns the socket. If the connection was closed before
     152              :     // (e.g. the client reset it while the response was pending; a client that only closed
     153              :     // its sending side has not left), the handler is not called, only released. A coroutine
     154              :     // handler runs on the io_context of the socket. Unless it switches protocols (101, or
     155              :     // 2xx to CONNECT) or has its own framing (Content-Length or Transfer-Encoding set with
     156              :     // header()), the response says "Connection: close": what the handler writes is read as
     157              :     // its body, until the connection closes.
     158              :     void take_over(takeover_handler handler);
     159              :     void take_over(takeover_coroutine handler);
     160              : 
     161              :     // File response, with the media type of its extension (application/octet-stream if it is
     162              :     // unknown or missing: nothing is guessed from its content), unless a Content-Type was set
     163              :     // before (header(), set_header()), as Node's send does. It carries a weak ETag and a
     164              :     // Last-Modified from the size and modification time of the file, and Cache-Control:
     165              :     // no-cache unless one was set before (header()): caches keep the file, but check it on
     166              :     // every use. GET and HEAD requests with If-None-Match (or else If-Modified-Since) are
     167              :     // answered with 304 Not Modified while it does not change; a GET with a single byte range
     168              :     // (Range, and If-Range with the Last-Modified date) with 206 Partial Content, or 416 if
     169              :     // it starts past the end. 404 if the file does not exist, 403 if it is not a regular file.
     170              :     // With force_download, Content-Disposition: attachment. A body (the file or its range) over
     171              :     // 1 MiB is streamed in pieces of 64 KiB at the pace of the client, unless it is a text file
     172              :     // compressed on the fly for the client; files over 4 MiB are never compressed on the fly.
     173              :     void send_file(const std::filesystem::path& path, bool force_download = false);
     174              : 
     175              :     // Redirect response. Each byte of `url` not allowed in a header value (CR, LF, NUL and the
     176              :     // other control characters but HTAB) is percent-encoded in Location; the rest is kept
     177              :     void redirect(const std::string& url, http::http_response::status redirect_type = http::http_response::status::moved_temporarily);
     178              :     
     179              :     // Chunked response: start_chunked() sends the headers (false if another answer was sent
     180              :     // first, or the connection is gone), then write_chunk() each chunk and end_chunked() the
     181              :     // last one. Chunks are only written on a response started with start_chunked(), once it
     182              :     // returned true (from any copy, any thread) and until end_chunked(): otherwise they
     183              :     // return false and write nothing. For a HEAD request, start_chunked() sends the headers
     184              :     // (without Transfer-Encoding) as a whole response, and the chunks are dropped.
     185              :     bool start_chunked(const std::string& content_type, http::http_response::status status = http::http_response::status::ok);
     186              :     bool write_chunk(const std::string& data);
     187              :     bool end_chunked();
     188              : 
     189              :     // Streamed response, with flow control (e.g. to relay a body as it arrives from
     190              :     // elsewhere). start_stream() sends the head: the headers set so far (header()) and those
     191              :     // of `head`, which replace the ones with the same name except Vary, whose fields add up,
     192              :     // with the status of `head`. Its framing is set here (Content-Length,
     193              :     // Transfer-Encoding and Connection headers in `head` are ignored): with `content_length`
     194              :     // the body has that length; without it, it is chunked, or, for HTTP/1.0 clients, ended
     195              :     // by closing the connection. Responses to HEAD requests, and with status 1xx, 204 or
     196              :     // 304, have no body (`content_length` is only advertised, except for 1xx and 204).
     197              :     // Then write_stream() each part of the body: it completes once the part is written to
     198              :     // the connection, so the body goes at the pace of the client. end_stream() completes
     199              :     // the response (false if the body is shorter than its Content-Length, then closing the
     200              :     // connection), or abort_stream() leaves it incomplete for the client (closing the
     201              :     // connection) if the body cannot be produced. They return false if the response cannot
     202              :     // be written: another answer was sent first, the connection is gone, more data than
     203              :     // its Content-Length or data for a response without body. Use one writer at a time.
     204              :     bool start_stream(const http_response& head, std::optional<size_t> content_length = std::nullopt);
     205              :     awaitable<bool> write_stream(std::string data);
     206              :     awaitable<bool> end_stream();
     207              :     void abort_stream();
     208              : 
     209              :     // Check if response has been sent
     210              :     bool has_responded() const;
     211              : 
     212              :     // Get the underlying connection (for advanced use cases); null for requests
     213              :     // dispatched in memory
     214              :     std::shared_ptr<server_connection> get_connection() const;
     215              : 
     216              : private:
     217              :     struct state;
     218              : 
     219              :     // The request timeout of the route (see route::timeout) is run by the application
     220              :     friend class http_application;
     221              :     // Files are answered by the file server, with the messages of this response
     222              :     friend void file_server::send(response& res, const file_server::representation& file);
     223              : 
     224              :     // The request answered, and the formatter of its errors (null for the default one)
     225              :     const http_request& request_message() const;
     226              :     const error_formatter* formatter() const;
     227              : 
     228              :     // The CORS policy of the application and what it means for this request, set before the
     229              :     // response is handed to the handler. Every answer applies them to its message, reading
     230              :     // nothing that may change: it may run on any thread
     231              :     void set_cors(std::shared_ptr<const detail::cors_policy> policy, detail::cors_decision decision);
     232              : 
     233              :     // Call `hook` once the response starts (the first answer), from the thread answering.
     234              :     // Set before the response is handed to the handler.
     235              :     void on_start(std::function<void()> hook);
     236              : 
     237              :     // Answer the request as timed out, with an error of `status`, unless another answer
     238              :     // started first (false then). Later answers are dropped quietly.
     239              :     // `with_headers`: with the status and headers set so far (false when another thread may
     240              :     // still be setting them, as for a request dispatched in memory)
     241              :     bool answer_timeout(http::http_response::status status, bool with_headers = true);
     242              : 
     243              :     // Whether the response was answered by answer_timeout()
     244              :     bool timed_out() const;
     245              : 
     246              :     // An answer arriving once the response was sent (or chunks out of place): logged as an
     247              :     // error, unless the request timed out (then the handler is just late)
     248              :     void log_late_answer(const char* message) const;
     249              : 
     250              :     bool ensure_not_responded() const;
     251              :     // Status and headers set before answering, created on first use
     252              :     http_response& draft();
     253              :     // Start answering: the message to send, with the status and headers set so far (the
     254              :     // draft, moved into it), if this is the first answer; otherwise null, and nothing must be
     255              :     // sent. What could fail building the answer (e.g. encoding its body) is done before
     256              :     std::shared_ptr<http_response> new_response();
     257              :     // The message of the answer started, from the draft (once), or without it
     258              :     std::shared_ptr<http_response> take_draft(bool with_draft = true);
     259              :     // Whether the request is HEAD: its response has no body
     260              :     bool head_request() const;
     261              :     // Body of a message about to be sent: framed (see frame_message), dropped for HEAD
     262              :     // (keeping its Content-Length), else compressed if `compress` and the client accepts it
     263              :     // (see compression::compress)
     264              :     void prepare_body(http_response& message, bool compress) const;
     265              :     // Framing of a message sent whole: its content is its body, with its Content-Length
     266              :     [[gnu::noinline, gnu::cold]] void frame_message(http_response& message) const;
     267              :     // Framing of the response of a connection taken over (see take_over): close if what the
     268              :     // handler writes is its body, ending when the connection closes, else none
     269              :     body_framing frame_takeover(http_response& message) const;
     270              :     // Mark the response as sent; false (and nothing must be sent) if it already was
     271              :     bool mark_responded();
     272              :     // Send the message of the answer started (see new_response(), prepare_body)
     273              :     void send_message(std::shared_ptr<http_response> message, bool compress = true);
     274              :     // Send the message of the answer started and hand the connection over to `handler`
     275              :     void hand_over(std::shared_ptr<http_response> message, takeover_handler handler);
     276              :     // Answers 501 if the connection cannot be taken over (in-memory requests)
     277              :     bool reject_takeover(const char* feature);
     278              :     // Upgrade to a WebSocket with a message interface, and to an event stream: `deliver`
     279              :     // receives the connection, and the socket it runs on (whose executor runs `deliver`)
     280              :     void accept_websocket(const websocket_accept_options& options,
     281              :                           std::function<void(std::shared_ptr<websocket_connection>, asio::websocket&)> deliver);
     282              :     void accept_sse(const sse_options& options,
     283              :                     std::function<void(std::shared_ptr<sse_connection>, asio::socket&)> deliver);
     284              :     // Checks of a WebSocket upgrade request and the subprotocol negotiated: the 101 response
     285              :     // to send, or null if the request was answered with an error
     286              :     std::shared_ptr<http_response> websocket_handshake(const std::vector<std::string>& protocols,
     287              :                                                        bool protocol_required, bool reject_unknown_protocol,
     288              :                                                        std::string& protocol);
     289              : 
     290              :     std::shared_ptr<state> state_;
     291              : };
     292              : 
     293              : } // namespace thinger::http
     294              : 
     295              : #endif // THINGER_HTTP_SERVER_RESPONSE_HPP
        

Generated by: LCOV version 2.0-1