From f55e69efb9573a52a251f63703d3c25f7cd40e3e Mon Sep 17 00:00:00 2001 From: Slavey Karadzhov Date: Fri, 26 Mar 2021 14:49:57 +0100 Subject: [PATCH 1/8] Added support for headers that can appear multiple times in one HTTP response or request. The following headers are supported: - Set-Cookie - WWW-Authenticate - Proxy-Authentica Updated HttpResponse::setCookie to support adding multiple cookies in one response. --- Sming/Core/Network/Http/HttpHeaderFields.cpp | 17 ++++++++++++ Sming/Core/Network/Http/HttpHeaderFields.h | 26 +++++++++++++++---- Sming/Core/Network/Http/HttpHeaders.h | 16 ++++++++++++ Sming/Core/Network/Http/HttpResponse.cpp | 9 +++++-- Sming/Core/Network/Http/HttpResponse.h | 2 +- .../HttpServer_Bootstrap/app/application.cpp | 5 ++++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Sming/Core/Network/Http/HttpHeaderFields.cpp b/Sming/Core/Network/Http/HttpHeaderFields.cpp index 66d5c2f39f..589cc1465d 100644 --- a/Sming/Core/Network/Http/HttpHeaderFields.cpp +++ b/Sming/Core/Network/Http/HttpHeaderFields.cpp @@ -35,6 +35,23 @@ String HttpHeaderFields::toString(HttpHeaderFieldName name) const return customFieldNames[unsigned(name) - unsigned(HTTP_HEADER_CUSTOM)]; } +String HttpHeaderFields::toString(HttpHeaderFieldName name, const String& value) const +{ + if(isMultiHeader(name)) { + String s; + Vector splits; + String values(value); + int m = splitString(values, '\0', splits); + for(int i = 0; i < m; i++) { + s += toString(toString(name), splits[i]); + } + + return s; + } + + return toString(toString(name), value); +} + String HttpHeaderFields::toString(const String& name, const String& value) { String s; diff --git a/Sming/Core/Network/Http/HttpHeaderFields.h b/Sming/Core/Network/Http/HttpHeaderFields.h index de05acd30b..e0b09b76ef 100644 --- a/Sming/Core/Network/Http/HttpHeaderFields.h +++ b/Sming/Core/Network/Http/HttpHeaderFields.h @@ -68,7 +68,8 @@ XX(UPGRADE, "Upgrade", \ "Used to transition from HTTP to some other protocol on the same connection. e.g. Websocket") \ XX(USER_AGENT, "User-Agent", "Information about the user agent originating the request") \ - XX(WWW_AUTHENTICATE, "WWW-Authenticate", "Indicates HTTP authentication scheme(s) and applicable parameters") + XX(WWW_AUTHENTICATE, "WWW-Authenticate", "Indicates HTTP authentication scheme(s) and applicable parameters") \ + XX(PROXY_AUTHENTICATE, "Proxy-Authenticate", "Indicates proxy authentication scheme(s) and applicable parameters") enum class HttpHeaderFieldName { UNKNOWN = 0, @@ -87,6 +88,24 @@ XX(CUSTOM, "", "") class HttpHeaderFields { public: + /** + * @brief Checks if a header is allowed to have multiple values + * @retval bool true if allowed + */ + bool isMultiHeader(HttpHeaderFieldName name) const + { + switch(name) { + case HTTP_HEADER_SET_COOKIE: + case HTTP_HEADER_WWW_AUTHENTICATE: + case HTTP_HEADER_PROXY_AUTHENTICATE: + break; + default: + return false; + } + + return true; + } + String toString(HttpHeaderFieldName name) const; /** @brief Produce a string for output in the HTTP header, with line ending @@ -96,10 +115,7 @@ class HttpHeaderFields */ static String toString(const String& name, const String& value); - String toString(HttpHeaderFieldName name, const String& value) const - { - return toString(toString(name), value); - } + String toString(HttpHeaderFieldName name, const String& value) const; /** @brief Find the enumerated value for the given field name string * @param name diff --git a/Sming/Core/Network/Http/HttpHeaders.h b/Sming/Core/Network/Http/HttpHeaders.h index 9c51d85857..ccdf80b3da 100644 --- a/Sming/Core/Network/Http/HttpHeaders.h +++ b/Sming/Core/Network/Http/HttpHeaders.h @@ -86,6 +86,22 @@ class HttpHeaders : public HttpHeaderFields, private HashMap Date: Fri, 26 Mar 2021 15:59:36 +0100 Subject: [PATCH 2/8] Added tests. --- tests/HostTests/modules/Http.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/HostTests/modules/Http.cpp b/tests/HostTests/modules/Http.cpp index c888abe2ac..59fe3e471f 100644 --- a/tests/HostTests/modules/Http.cpp +++ b/tests/HostTests/modules/Http.cpp @@ -195,6 +195,19 @@ class HttpTest : public TestGroup REQUIRE(serialize(headers2) == FS_serialized); printHeaders(headers2); } + + DEFINE_FSTR_LOCAL(FS_cookies, "Set-Cookie: name1=value1\r\n" + "Set-Cookie: name2=value2\r\n"); + + TEST_CASE("appendHeaders") + { + HttpHeaders headers2; + headers2.append(HTTP_HEADER_SET_COOKIE, "name1=value1"); + headers2.append(HTTP_HEADER_SET_COOKIE, "name2=value2"); + REQUIRE(headers2.count() == 1); + REQUIRE(serialize(headers2) == FS_cookies); + printHeaders(headers2); + } } }; From 560bbd53021a858b91261b25c94a90f479529a92 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 26 Mar 2021 19:59:28 +0000 Subject: [PATCH 3/8] Add flags to HTTP_HEADER_FIELDNAME_MAP and use to identify MULTI fields --- Sming/Core/Network/Http/HttpHeaderFields.cpp | 20 +++- Sming/Core/Network/Http/HttpHeaderFields.h | 107 ++++++++++--------- Sming/Core/Network/Http/HttpHeaders.h | 2 +- 3 files changed, 74 insertions(+), 55 deletions(-) diff --git a/Sming/Core/Network/Http/HttpHeaderFields.cpp b/Sming/Core/Network/Http/HttpHeaderFields.cpp index 589cc1465d..6ab6f39075 100644 --- a/Sming/Core/Network/Http/HttpHeaderFields.cpp +++ b/Sming/Core/Network/Http/HttpHeaderFields.cpp @@ -14,14 +14,28 @@ #include // Define field name strings and a lookup table -#define XX(_tag, _str, _comment) DEFINE_FSTR_LOCAL(hhfnStr_##_tag, _str); +#define XX(tag, str, flags, comment) DEFINE_FSTR_LOCAL(hhfnStr_##tag, str); HTTP_HEADER_FIELDNAME_MAP(XX) #undef XX -#define XX(_tag, _str, _comment) &hhfnStr_##_tag, +#define XX(tag, str, flags, comment) &hhfnStr_##tag, DEFINE_FSTR_VECTOR_LOCAL(fieldNameStrings, FlashString, HTTP_HEADER_FIELDNAME_MAP(XX)); #undef XX +HttpHeaderFields::Flags HttpHeaderFields::getFlags(HttpHeaderFieldName name) const +{ + switch(name) { +#define XX(tag, str, flags, comment) \ + case HttpHeaderFieldName::tag: \ + return flags; + HTTP_HEADER_FIELDNAME_MAP(XX) +#undef XX + default: + // Custom fields + return 0; + } +} + String HttpHeaderFields::toString(HttpHeaderFieldName name) const { if(name == HTTP_HEADER_UNKNOWN) { @@ -37,7 +51,7 @@ String HttpHeaderFields::toString(HttpHeaderFieldName name) const String HttpHeaderFields::toString(HttpHeaderFieldName name, const String& value) const { - if(isMultiHeader(name)) { + if(getFlags(name)[Flag::Multi]) { String s; Vector splits; String values(value); diff --git a/Sming/Core/Network/Http/HttpHeaderFields.h b/Sming/Core/Network/Http/HttpHeaderFields.h index e0b09b76ef..2ec1c14431 100644 --- a/Sming/Core/Network/Http/HttpHeaderFields.h +++ b/Sming/Core/Network/Http/HttpHeaderFields.h @@ -14,6 +14,7 @@ #include "Data/CStringArray.h" #include "WString.h" +#include /* * Common HTTP header field names. Enumerating these simplifies matching @@ -25,86 +26,90 @@ * A brief description of each header field is given for information purposes. * For details see https://www.iana.org/assignments/message-headers/message-headers.xhtml * + * Entries are formatted thus: XX(tag, str, flags, comment) + * tag: Identifier used in code (without HTTP_HEADER_ prefix) + * str: String used in HTTP protocol + * flags: Additional flags (see HttpHeaderFields::Flag) + * comment: Further details */ #define HTTP_HEADER_FIELDNAME_MAP(XX) \ - XX(ACCEPT, "Accept", "Limit acceptable response types") \ - XX(ACCESS_CONTROL_ALLOW_ORIGIN, "Access-Control-Allow-Origin", "") \ - XX(AUTHORIZATION, "Authorization", "Basic user agent authentication") \ - XX(CC, "Cc", "email field") \ - XX(CONNECTION, "Connection", "Indicates sender's desired control options") \ - XX(CONTENT_DISPOSITION, "Content-Disposition", "Additional information about how to process response payload") \ - XX(CONTENT_ENCODING, "Content-Encoding", "Applied encodings in addition to content type") \ - XX(CONTENT_LENGTH, "Content-Length", "Anticipated size for payload when not using transfer encoding") \ - XX(CONTENT_TYPE, "Content-Type", \ + XX(ACCEPT, "Accept", 0, "Limit acceptable response types") \ + XX(ACCESS_CONTROL_ALLOW_ORIGIN, "Access-Control-Allow-Origin", 0, "") \ + XX(AUTHORIZATION, "Authorization", 0, "Basic user agent authentication") \ + XX(CC, "Cc", 0, "email field") \ + XX(CONNECTION, "Connection", 0, "Indicates sender's desired control options") \ + XX(CONTENT_DISPOSITION, "Content-Disposition", 0, "Additional information about how to process response payload") \ + XX(CONTENT_ENCODING, "Content-Encoding", 0, "Applied encodings in addition to content type") \ + XX(CONTENT_LENGTH, "Content-Length", 0, "Anticipated size for payload when not using transfer encoding") \ + XX(CONTENT_TYPE, "Content-Type", 0, \ "Payload media type indicating both data format and intended manner of processing by recipient") \ - XX(CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", "Coding method used in a MIME message body part") \ - XX(CACHE_CONTROL, "Cache-Control", "Directives for caches along the request/response chain") \ - XX(DATE, "Date", "Message originating date/time") \ - XX(EXPECT, "Expect", "Behaviours to be supported by the server in order to properly handle this request.") \ - XX(ETAG, "ETag", \ + XX(CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", 0, "Coding method used in a MIME message body part") \ + XX(CACHE_CONTROL, "Cache-Control", 0, "Directives for caches along the request/response chain") \ + XX(DATE, "Date", 0, "Message originating date/time") \ + XX(EXPECT, "Expect", 0, "Behaviours to be supported by the server in order to properly handle this request.") \ + XX(ETAG, "ETag", 0, \ "Validates resource, such as a file, so recipient can confirm whether it has changed - generally more " \ "reliable than Date") \ - XX(FROM, "From", "email address of human user who controls the requesting user agent") \ - XX(HOST, "Host", \ + XX(FROM, "From", 0, "email address of human user who controls the requesting user agent") \ + XX(HOST, "Host", 0, \ "Request host and port information for target URI; allows server to service requests for multiple hosts on a " \ "single IP address") \ - XX(IF_MATCH, "If-Match", \ + XX(IF_MATCH, "If-Match", 0, \ "Precondition check using ETag to avoid accidental overwrites when servicing multiple user requests. Ensures " \ "resource entity tag matches before proceeding.") \ - XX(IF_MODIFIED_SINCE, "If-Modified-Since", "Precondition check using Date") \ - XX(LAST_MODIFIED, "Last-Modified", "Server timestamp indicating date and time resource was last modified") \ - XX(LOCATION, "Location", "Used in redirect responses, amongst other places") \ - XX(SEC_WEBSOCKET_ACCEPT, "Sec-WebSocket-Accept", "Server response to opening Websocket handshake") \ - XX(SEC_WEBSOCKET_VERSION, "Sec-WebSocket-Version", \ + XX(IF_MODIFIED_SINCE, "If-Modified-Since", 0, "Precondition check using Date") \ + XX(LAST_MODIFIED, "Last-Modified", 0, "Server timestamp indicating date and time resource was last modified") \ + XX(LOCATION, "Location", 0, "Used in redirect responses, amongst other places") \ + XX(SEC_WEBSOCKET_ACCEPT, "Sec-WebSocket-Accept", 0, "Server response to opening Websocket handshake") \ + XX(SEC_WEBSOCKET_VERSION, "Sec-WebSocket-Version", 0, \ "Websocket opening request indicates acceptable protocol version. Can appear more than once.") \ - XX(SEC_WEBSOCKET_KEY, "Sec-WebSocket-Key", "Websocket opening request validation key") \ - XX(SEC_WEBSOCKET_PROTOCOL, "Sec-WebSocket-Protocol", \ + XX(SEC_WEBSOCKET_KEY, "Sec-WebSocket-Key", 0, "Websocket opening request validation key") \ + XX(SEC_WEBSOCKET_PROTOCOL, "Sec-WebSocket-Protocol", 0, \ "Websocket opening request indicates supported protocol(s), response contains negotiated protocol(s)") \ - XX(SERVER, "Server", "Identifies software handling requests") \ - XX(SET_COOKIE, "Set-Cookie", "Server may pass name/value pairs and associated metadata to user agent (client)") \ - XX(SUBJECT, "Subject", "email subject line") \ - XX(TO, "To", "email intended recipient address") \ - XX(TRANSFER_ENCODING, "Transfer-Encoding", "e.g. Chunked, compress, deflate, gzip") \ - XX(UPGRADE, "Upgrade", \ + XX(SERVER, "Server", 0, "Identifies software handling requests") \ + XX(SET_COOKIE, "Set-Cookie", Flag::Multi, \ + "Server may pass name/value pairs and associated metadata to user agent (client)") \ + XX(SUBJECT, "Subject", 0, "email subject line") \ + XX(TO, "To", 0, "email intended recipient address") \ + XX(TRANSFER_ENCODING, "Transfer-Encoding", 0, "e.g. Chunked, compress, deflate, gzip") \ + XX(UPGRADE, "Upgrade", 0, \ "Used to transition from HTTP to some other protocol on the same connection. e.g. Websocket") \ - XX(USER_AGENT, "User-Agent", "Information about the user agent originating the request") \ - XX(WWW_AUTHENTICATE, "WWW-Authenticate", "Indicates HTTP authentication scheme(s) and applicable parameters") \ - XX(PROXY_AUTHENTICATE, "Proxy-Authenticate", "Indicates proxy authentication scheme(s) and applicable parameters") + XX(USER_AGENT, "User-Agent", 0, "Information about the user agent originating the request") \ + XX(WWW_AUTHENTICATE, "WWW-Authenticate", Flag::Multi, \ + "Indicates HTTP authentication scheme(s) and applicable parameters") \ + XX(PROXY_AUTHENTICATE, "Proxy-Authenticate", Flag::Multi, \ + "Indicates proxy authentication scheme(s) and applicable parameters") enum class HttpHeaderFieldName { UNKNOWN = 0, -#define XX(tag, str, comment) tag, +#define XX(tag, str, flags, comment) tag, HTTP_HEADER_FIELDNAME_MAP(XX) #undef XX CUSTOM // First custom header tag value }; -#define XX(tag, str, comment) constexpr HttpHeaderFieldName HTTP_HEADER_##tag = HttpHeaderFieldName::tag; -XX(UNKNOWN, "", "") +#define XX(tag, str, flags, comment) constexpr HttpHeaderFieldName HTTP_HEADER_##tag = HttpHeaderFieldName::tag; +XX(UNKNOWN, "", 0, "") HTTP_HEADER_FIELDNAME_MAP(XX) -XX(CUSTOM, "", "") +XX(CUSTOM, "", 0, "") #undef XX class HttpHeaderFields { public: /** - * @brief Checks if a header is allowed to have multiple values - * @retval bool true if allowed + * @brief Flag values providing additional information about header fields */ - bool isMultiHeader(HttpHeaderFieldName name) const - { - switch(name) { - case HTTP_HEADER_SET_COOKIE: - case HTTP_HEADER_WWW_AUTHENTICATE: - case HTTP_HEADER_PROXY_AUTHENTICATE: - break; - default: - return false; - } + enum class Flag { + Multi, ///< Field may have multiple values + }; + using Flags = BitSet; - return true; - } + /** + * @brief Get flags (if any) for given header field + * @retval Flags + */ + Flags getFlags(HttpHeaderFieldName name) const; String toString(HttpHeaderFieldName name) const; diff --git a/Sming/Core/Network/Http/HttpHeaders.h b/Sming/Core/Network/Http/HttpHeaders.h index ccdf80b3da..34911f1cbe 100644 --- a/Sming/Core/Network/Http/HttpHeaders.h +++ b/Sming/Core/Network/Http/HttpHeaders.h @@ -88,7 +88,7 @@ class HttpHeaders : public HttpHeaderFields, private HashMap Date: Fri, 26 Mar 2021 21:17:56 +0000 Subject: [PATCH 4/8] Always succeed `HttpHeader::append()` if field value not already set --- Sming/Core/Network/Http/HttpHeaders.h | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Sming/Core/Network/Http/HttpHeaders.h b/Sming/Core/Network/Http/HttpHeaders.h index 34911f1cbe..6439a8691a 100644 --- a/Sming/Core/Network/Http/HttpHeaders.h +++ b/Sming/Core/Network/Http/HttpHeaders.h @@ -86,19 +86,27 @@ class HttpHeaders : public HttpHeaderFields, private HashMap Date: Fri, 26 Mar 2021 20:58:14 +0000 Subject: [PATCH 5/8] Reverse logic, simpler --- Sming/Core/Network/Http/HttpResponse.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sming/Core/Network/Http/HttpResponse.cpp b/Sming/Core/Network/Http/HttpResponse.cpp index 01e055c585..0330e875bc 100644 --- a/Sming/Core/Network/Http/HttpResponse.cpp +++ b/Sming/Core/Network/Http/HttpResponse.cpp @@ -20,10 +20,10 @@ HttpResponse* HttpResponse::setCookie(const String& name, const String& value, b String s = name; s += '='; s += value; - if(!append) { - headers[HTTP_HEADER_SET_COOKIE] = s; - } else { + if(append) { headers.append(HTTP_HEADER_SET_COOKIE, s); + } else { + headers[HTTP_HEADER_SET_COOKIE] = s; } return this; From 0d13bd3bdd6b3c38e9b6aa0233dce6719d9bc6c7 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 26 Mar 2021 20:59:08 +0000 Subject: [PATCH 6/8] Move longer `HttpHeaders` methods into .cpp file --- Sming/Core/Network/Http/HttpHeaders.cpp | 48 +++++++++++++++++++++++++ Sming/Core/Network/Http/HttpHeaders.h | 36 ++----------------- 2 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 Sming/Core/Network/Http/HttpHeaders.cpp diff --git a/Sming/Core/Network/Http/HttpHeaders.cpp b/Sming/Core/Network/Http/HttpHeaders.cpp new file mode 100644 index 0000000000..9f98985693 --- /dev/null +++ b/Sming/Core/Network/Http/HttpHeaders.cpp @@ -0,0 +1,48 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * HttpHeaders.cpp + * + ****/ + +#include "HttpHeaders.h" +#include + +const String& HttpHeaders::operator[](const String& name) const +{ + auto field = fromString(name); + if(field == HTTP_HEADER_UNKNOWN) { + return nil; + } + return operator[](field); +} + +bool HttpHeaders::append(const HttpHeaderFieldName& name, const String& value) +{ + int i = indexOf(name); + if(i < 0) { + operator[](name) = value; + return true; + } + + if(!getFlags(name)[Flag::Multi]) { + debug_w("[HTTP] Append not supported for header field '%s'", toString(name).c_str()); + return false; + } + + valueAt(i) += '\0' + value; + + return true; +} + +void HttpHeaders::setMultiple(const HttpHeaders& headers) +{ + for(unsigned i = 0; i < headers.count(); i++) { + HttpHeaderFieldName fieldName = headers.keyAt(i); + auto fieldNameString = headers.toString(fieldName); + operator[](fieldNameString) = headers.valueAt(i); + } +} diff --git a/Sming/Core/Network/Http/HttpHeaders.h b/Sming/Core/Network/Http/HttpHeaders.h index 6439a8691a..0c3fd579e4 100644 --- a/Sming/Core/Network/Http/HttpHeaders.h +++ b/Sming/Core/Network/Http/HttpHeaders.h @@ -48,14 +48,7 @@ class HttpHeaders : public HttpHeaderFields, private HashMap Date: Fri, 26 Mar 2021 21:11:22 +0000 Subject: [PATCH 7/8] Optimise `HttpHeaderFields::toString()` for multi values --- Sming/Core/Network/Http/HttpHeaderFields.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Sming/Core/Network/Http/HttpHeaderFields.cpp b/Sming/Core/Network/Http/HttpHeaderFields.cpp index 6ab6f39075..5096b576ed 100644 --- a/Sming/Core/Network/Http/HttpHeaderFields.cpp +++ b/Sming/Core/Network/Http/HttpHeaderFields.cpp @@ -51,19 +51,22 @@ String HttpHeaderFields::toString(HttpHeaderFieldName name) const String HttpHeaderFields::toString(HttpHeaderFieldName name, const String& value) const { + String tag = toString(name); if(getFlags(name)[Flag::Multi]) { + tag += ": "; + CStringArray values(value); String s; - Vector splits; - String values(value); - int m = splitString(values, '\0', splits); - for(int i = 0; i < m; i++) { - s += toString(toString(name), splits[i]); + s.reserve((tag.length() + 2) * values.count() + value.length()); + for(auto p : values) { + s += tag; + s += p; + s += "\r\n"; } return s; } - return toString(toString(name), value); + return toString(tag, value); } String HttpHeaderFields::toString(const String& name, const String& value) From 16af618ed4231654da7a5193393acb9e1f31fa52 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Fri, 26 Mar 2021 20:56:46 +0000 Subject: [PATCH 8/8] Update tests --- tests/HostTests/modules/Http.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/HostTests/modules/Http.cpp b/tests/HostTests/modules/Http.cpp index 59fe3e471f..607c9c2805 100644 --- a/tests/HostTests/modules/Http.cpp +++ b/tests/HostTests/modules/Http.cpp @@ -35,11 +35,12 @@ class HttpTest : public TestGroup TEST_CASE("http lookups") { auto s = toString(HPE_UNKNOWN); - REQUIRE(s == F("HPE_UNKNOWN")); + REQUIRE(s == "HPE_UNKNOWN"); s = httpGetErrorDescription(HPE_INVALID_URL); - REQUIRE(s == F("invalid URL")); + REQUIRE(s == "invalid URL"); s = toString(HTTP_STATUS_TOO_MANY_REQUESTS); - REQUIRE(s.equalsIgnoreCase(F("too many requests"))); + DEFINE_FSTR_LOCAL(too_many_requests, "too many requests"); + REQUIRE(s.equalsIgnoreCase(too_many_requests)); } } @@ -204,9 +205,14 @@ class HttpTest : public TestGroup HttpHeaders headers2; headers2.append(HTTP_HEADER_SET_COOKIE, "name1=value1"); headers2.append(HTTP_HEADER_SET_COOKIE, "name2=value2"); + printHeaders(headers2); REQUIRE(headers2.count() == 1); REQUIRE(serialize(headers2) == FS_cookies); - printHeaders(headers2); + + // Append should work if field not already set + REQUIRE(headers2.append(HTTP_HEADER_CONTENT_LENGTH, "0") == true); + // But fail on actual append + REQUIRE(headers2.append(HTTP_HEADER_CONTENT_LENGTH, "1234") == false); } } };