Request Bodies

A request body is attached with the request_builder::body function. The type of the value decides three things: the Content-Type header, whether the body is sent with a known Content-Length or with chunked transfer encoding, and how the bytes are produced on the wire.

How the Body Is Framed

body accepts any value for which a conversion is defined, by calling tag_invoke with a body_from_tag<T>; the result is an any_request_body. Two rules then hold for every body type:

  • The body’s Content-Type is sent unless the request already sets that header explicitly — setting http::field::content_type on the request always wins.

  • The framing — a Content-Length, or chunked transfer encoding — is always taken from the body and cannot be overridden. A body whose size is known in advance is sent with a Content-Length; one whose size is not is sent chunked.

Built-in Body Types

Strings

A std::string, a std::string_view, or a string literal is sent as text/plain; charset=utf-8 with a Content-Length:

auto [ec, r] = co_await client.post("https://example.com/post")
    .body("plain text payload")
    .send();

The default Content-Type can be overridden by setting the header explicitly:

auto [ec, r] = co_await client.post("https://example.com/post")
    .body("<note>hi</note>")
    .header(http::field::content_type, "application/xml")
    .send();

JSON

The Boost.JSON types are sent as application/json, serialized incrementally as the request goes out. Their serialized size is not known beforehand, so this is the one built-in body sent with chunked transfer encoding:

json::object obj({ { "user", "John" }, { "lang", "En" } });

auto [ec, r] = co_await client.post("https://example.com/post")
    .body(obj)
    .send();

Naming the type builds the value inline, with no named variable:

auto [ec, r] = co_await client.post("https://example.com/post")
    .body<json::array>({ 1, 2, 3 })
    .send();

URL-Encoded Forms

urlencoded_form builds an application/x-www-form-urlencoded body from name/value pairs, sent with a Content-Length. append chains, and the same name may be added more than once to submit a multi-valued field:

auto [ec, r] = co_await client.post("https://example.com/post")
    .body(burl::urlencoded_form()
        .append("user", "John")
        .append("color", "blue")
        .append("color", "green"))
    .send();
// user=John&color=blue&color=green

Names and values are percent-encoded: only the RFC 3986 unreserved characters pass through, spaces become +, and the & and = separators are escaped, so a value can never break out of the field structure. A form can also be built from an initializer list or any range of pairs:

std::map<std::string, std::string> fields = {
    { "lang", "En" },
    { "user", "John" } };

auto [ec, r] = co_await client.post("https://example.com/post")
    .body<burl::urlencoded_form>(fields)
    .send();

Multipart Forms

multipart_form builds a multipart/form-data body from a sequence of parts separated by a generated boundary, as specified by RFC 7578:

auto [ec, r] = co_await client.post("https://example.com/upload")
    .body(burl::multipart_form()
        .text("priority", "high")
        .file("attachment", "./report.log"))
    .send();

text adds a plain field, while file adds a file that is streamed from disk as the request is sent; only the path is held, with its filename and Content-Type deduced from it unless set explicitly. When the data is already in memory but should still arrive as a file, bytes writes a part with a filename from an in-memory buffer.

Files

Naming std::filesystem::path uploads a file, streamed from disk so the whole file is never held in memory. The Content-Type is deduced from the filename extension, defaulting to application/octet-stream, and the Content-Length is the file’s size:

auto [ec, r] = co_await client.put("https://example.com/put")
    .body<std::filesystem::path>("./report.log")
    .send();

The size is read when request_builder::body is called, so a path whose size cannot be determined throws std::filesystem::filesystem_error from that call. Because the length is then fixed, a file that shrinks before the transfer completes fails the request with error::file_changed.

Your Own Types

request_builder::body accepts any type for which a tag_invoke conversion is defined. Extending shows how to write one for a type of your own.

Next Steps