diff --git a/CHANGELOG.md b/CHANGELOG.md index f4933730..91444608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Version 49 * Use instead of +HTTP: + +* Add HEAD request example + API Changes: * Refactor method and verb diff --git a/doc/4_5_parser_streams.qbk b/doc/4_5_parser_streams.qbk index 6b2fa291..267ffa6a 100644 --- a/doc/4_5_parser_streams.qbk +++ b/doc/4_5_parser_streams.qbk @@ -214,6 +214,77 @@ receive_expect_100_continue( } ``` +[heading Example: HEAD request (Client)] +``` +/** Send a HEAD request for a resource. + + This function submits a HEAD request for the specified resource + and returns the response. + + @param res The response. This is an output parameter. + + @param stream The synchronous stream to use. + + @param buffer The buffer to use. + + @param target The request target. + + @param ec Set to the error, if any occurred. + + @throws std::invalid_argument if target is empty. +*/ +template< + class SyncStream, + class DynamicBuffer +> +response +do_head_request( + SyncStream& stream, + DynamicBuffer& buffer, + string_view target, + error_code& ec) +{ + // Do some type checking to be a good citizen + static_assert(is_sync_stream::value, + "SyncStream requirements not met"); + static_assert(is_dynamic_buffer::value, + "DynamicBuffer requirments not met"); + + // The interfaces we are using are low level and do not + // perform any checking of arguments; so we do it here. + if(target.empty()) + throw std::invalid_argument("target may not be empty"); + + // Build the HEAD request for the target + request req; + req.version = 11; + req.method(verb::head); + req.target(target); + req.fields.insert("User-Agent", "test"); + + // A client MUST send a Host header field in all HTTP/1.1 request messages. + // https://tools.ietf.org/html/rfc7230#section-5.4 + req.fields.insert("Host", "localhost"); + + // Now send it + write(stream, req, ec); + if(ec) + return {}; + + // Create a parser to read the response. + // Responses to HEAD requests MUST NOT include + // a body, so we use the `empty_body` type and + // only attempt to read the header. + parser p; + read_header(stream, buffer, p, ec); + if(ec) + return {}; + + // Transfer ownership of the response to the caller. + return p.release(); +} +``` + [heading Example: HTTP Relay] An HTTP proxy acts as a relay between client and server. The proxy reads a diff --git a/include/beast/core/ostream.hpp b/include/beast/core/ostream.hpp index b2efbb05..49cbd69a 100644 --- a/include/beast/core/ostream.hpp +++ b/include/beast/core/ostream.hpp @@ -48,7 +48,7 @@ buffers(ConstBufferSequence const& b) { static_assert(is_const_buffer_sequence< ConstBufferSequence>::value, - "ConstBufferSequence not met"); + "ConstBufferSequence requirements not met"); return detail::buffers_helper< ConstBufferSequence>{b}; } diff --git a/test/http/design.cpp b/test/http/design.cpp index 2aef02f9..c5084809 100644 --- a/test/http/design.cpp +++ b/test/http/design.cpp @@ -783,6 +783,171 @@ public: } } + //-------------------------------------------------------------------------- + // + // Example: HEAD Request + // + //-------------------------------------------------------------------------- + + /** Handle a HEAD request for a resource. + */ + template< + class SyncStream, + class DynamicBuffer + > + void do_server_head( + SyncStream& stream, + DynamicBuffer& buffer, + error_code& ec) + { + static_assert(is_sync_stream::value, + "SyncStream requirements not met"); + static_assert(is_dynamic_buffer::value, + "DynamicBuffer requirments not met"); + + // We deliver this payload for all GET requests + static std::string const payload = "Hello, world!"; + + // Read the request + request req; + read(stream, buffer, req, ec); + if(ec) + return; + + // Set up the response, starting with the common fields + response res; + res.version = 11; + res.fields.insert("Server", "test"); + + // Now handle request-specific fields + switch(req.method()) + { + case verb::head: + case verb::get: + { + // A HEAD request is handled by delivering the same + // set of headers that would be sent for a GET request, + // including the Content-Length, except for the body. + res.result(status::ok); + res.fields.insert("Content-Length", payload.size()); + + // For GET requests, we include the body + if(req.method() == verb::get) + { + // We deliver the same payload for GET requests + // regardless of the target. A real server might + // deliver a file based on the target. + res.body = payload; + } + break; + } + + default: + { + // We return responses indicating an error if + // we do not recognize the request method. + res.result(status::bad_request); + res.fields.insert("Content-Type", "text/plain"); + res.body = "Invalid request-method '" + req.method_string().to_string() + "'"; + break; + } + } + + // Send the response + write(stream, res, ec); + if(ec) + return; + } + + /** Send a HEAD request for a resource. + + This function submits a HEAD request for the specified resource + and returns the response. + + @param res The response. This is an output parameter. + + @param stream The synchronous stream to use. + + @param buffer The buffer to use. + + @param target The request target. + + @param ec Set to the error, if any occurred. + + @throws std::invalid_argument if target is empty. + */ + template< + class SyncStream, + class DynamicBuffer + > + response + do_head_request( + SyncStream& stream, + DynamicBuffer& buffer, + string_view target, + error_code& ec) + { + // Do some type checking to be a good citizen + static_assert(is_sync_stream::value, + "SyncStream requirements not met"); + static_assert(is_dynamic_buffer::value, + "DynamicBuffer requirments not met"); + + // The interfaces we are using are low level and do not + // perform any checking of arguments; so we do it here. + if(target.empty()) + throw std::invalid_argument("target may not be empty"); + + // Build the HEAD request for the target + request req; + req.version = 11; + req.method(verb::head); + req.target(target); + req.fields.insert("User-Agent", "test"); + + // A client MUST send a Host header field in all HTTP/1.1 request messages. + // https://tools.ietf.org/html/rfc7230#section-5.4 + req.fields.insert("Host", "localhost"); + + // Now send it + write(stream, req, ec); + if(ec) + return {}; + + // Create a parser to read the response. + // Responses to HEAD requests MUST NOT include + // a body, so we use the `empty_body` type and + // only attempt to read the header. + parser p; + read_header(stream, buffer, p, ec); + if(ec) + return {}; + + // Transfer ownership of the response to the caller. + return p.release(); + } + + void + doHEAD() + { + test::pipe p{ios_}; + yield_to( + [&](yield_context) + { + error_code ec; + flat_buffer buffer; + do_server_head(p.server, buffer, ec); + BEAST_EXPECTS(! ec, ec.message()); + }, + [&](yield_context) + { + error_code ec; + flat_buffer buffer; + auto res = do_head_request(p.client, buffer, "/", ec); + BEAST_EXPECTS(! ec, ec.message()); + }); + } + //-------------------------------------------------------------------------- // // Deferred Body type commitment @@ -954,6 +1119,7 @@ public: doRelay(); doParseStdStream(); doCustomParser(); + doHEAD(); doDeferredBody(); }