Network System 0.1.1
High-performance modular networking library for scalable client-server applications
Loading...
Searching...
No Matches
http_server.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2024, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
8#include <algorithm>
9#include <cctype>
10#include <sstream>
11
12namespace kcenon::network::core {
13// http_route implementation
14
16 internal::http_method req_method, const std::string &path,
17 std::map<std::string, std::string> &extracted_params) const -> bool {
18 // Check method
19 if (method != req_method) {
20 return false;
21 }
22
23 // Try to match path with regex
24 std::smatch matches;
25 if (!std::regex_match(path, matches, regex_pattern)) {
26 return false;
27 }
28
29 // Extract path parameters
30 extracted_params.clear();
31 for (std::size_t i = 0; i < param_names.size() && i + 1 < matches.size();
32 ++i) {
33 extracted_params[param_names[i]] = matches[i + 1].str();
34 }
35
36 return true;
37}
38
39// http_request_buffer implementation
40
41auto http_request_buffer::append(const std::vector<uint8_t> &chunk) -> bool {
42 // Check size limits
43 if (data.size() + chunk.size() > MAX_REQUEST_SIZE) {
44 return false; // 413 Payload Too Large
45 }
46
47 // Append chunk to buffer
48 data.insert(data.end(), chunk.begin(), chunk.end());
49
50 // Find headers end if not yet found
51 if (!headers_complete) {
52 auto marker_pos = find_header_end(data);
53 if (marker_pos != std::string::npos) {
54 headers_complete = true;
55 headers_end_pos = marker_pos + 4; // "\r\n\r\n" length
56 content_length = parse_content_length(data, headers_end_pos);
57 } else if (data.size() > MAX_HEADER_SIZE) {
58 return false; // 431 Request Header Fields Too Large
59 }
60 }
61
62 return true;
63}
64
67}
68
69auto http_request_buffer::find_header_end(const std::vector<uint8_t> &data)
70 -> std::size_t {
71 // Look for "\r\n\r\n" sequence
72 for (std::size_t i = 0; i + 3 < data.size(); ++i) {
73 if (data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' &&
74 data[i + 3] == '\n') {
75 return i;
76 }
77 }
78 return std::string::npos;
79}
80
81auto http_request_buffer::parse_content_length(const std::vector<uint8_t> &data,
82 std::size_t headers_end)
83 -> std::size_t {
84 // Convert headers to string for parsing
85 std::string headers_str(reinterpret_cast<const char *>(data.data()),
86 std::min(headers_end, data.size()));
87
88 // Look for Content-Length header (case-insensitive)
89 const std::string content_length_key = "content-length:";
90 std::size_t pos = 0;
91
92 while (pos < headers_str.size()) {
93 // Find line start
94 std::size_t line_start = pos;
95 std::size_t line_end = headers_str.find("\r\n", pos);
96 if (line_end == std::string::npos) {
97 break;
98 }
99
100 std::string line = headers_str.substr(line_start, line_end - line_start);
101
102 // Convert to lowercase for case-insensitive comparison
103 std::string line_lower = line;
104 std::transform(line_lower.begin(), line_lower.end(), line_lower.begin(),
105 [](unsigned char c) { return std::tolower(c); });
106
107 // Check if this is Content-Length header
108 if (line_lower.find(content_length_key) == 0) {
109 // Extract value after colon
110 std::size_t colon_pos = line.find(':');
111 if (colon_pos != std::string::npos) {
112 std::string value = line.substr(colon_pos + 1);
113 // Trim whitespace
114 value.erase(0, value.find_first_not_of(" \t"));
115 value.erase(value.find_last_not_of(" \t\r\n") + 1);
116
117 try {
118 return std::stoull(value);
119 } catch (...) {
120 return 0;
121 }
122 }
123 }
124
125 pos = line_end + 2; // Skip "\r\n"
126 }
127
128 return 0; // No Content-Length header found
129}
130
131// http_server implementation
132
133http_server::http_server(const std::string &server_id)
134 : tcp_server_(std::make_shared<messaging_server>(server_id)) {
135 // Set up default 404 handler
138 return create_error_response(404, "Not Found");
139 };
140
141 // Set up default 500 handler
144 return create_error_response(500, "Internal Server Error");
145 };
146}
147
149 if (tcp_server_) {
150 (void)tcp_server_->stop_server();
151 }
152}
153
154auto http_server::start(unsigned short port) -> VoidResult {
155 // Set up receive callback to handle HTTP requests with buffering
156 tcp_server_->set_receive_callback(
157 [this](
158 std::shared_ptr<kcenon::network::session::messaging_session> session,
159 const std::vector<uint8_t> &data) {
160 if (!session) {
161 return;
162 }
163
164 // Get or create buffer for this session
165 std::unique_lock<std::mutex> lock(buffers_mutex_);
166 auto &buffer = session_buffers_[session];
167 lock.unlock();
168
169 // Append new data to buffer
170 if (!buffer.append(data)) {
171 // Buffer size limit exceeded
172 lock.lock();
173 session_buffers_.erase(session);
174 lock.unlock();
175
176 // Send error response
177 if (buffer.data.size() > http_request_buffer::MAX_REQUEST_SIZE) {
178 auto error_response =
179 create_error_response(413, "Payload Too Large");
180 auto response_data =
182 session->send_packet(std::move(response_data));
183 } else {
184 auto error_response =
185 create_error_response(431, "Request Header Fields Too Large");
186 auto response_data =
188 session->send_packet(std::move(response_data));
189 }
190 return;
191 }
192
193 // Check if request is complete
194 if (buffer.is_complete()) {
195 // Parse HTTP request
196 auto request_result =
198
199 // Clean up buffer first
200 lock.lock();
201 session_buffers_.erase(session);
202 lock.unlock();
203
204 if (request_result.is_err()) {
205 // Failed to parse - send 400 Bad Request
206 auto error_response = create_error_response(400, "Bad Request");
207 auto response_data =
209 session->send_packet(std::move(response_data));
210 // Close connection on parse error
211 session->stop_session();
212 return;
213 }
214
215 auto http_request = std::move(request_result.value());
216
217 // Process request and generate response
218 auto response = process_http_request(http_request);
219
220 // Determine if connection should be closed
221 bool close_conn = should_close_connection(http_request, response);
222
223 // Serialize and send response
224 auto response_data =
226 session->send_packet(std::move(response_data));
227
228 // Close connection if required
229 if (close_conn) {
230 // Give time for response to be sent before closing
231 // In a production system, we would wait for send completion
232 // callback
233 session->stop_session();
234 }
235 }
236 // Otherwise, wait for more data
237 });
238
239 // Start TCP server
240 return tcp_server_->start_server(port);
241}
242
243auto http_server::stop() -> VoidResult { return tcp_server_->stop_server(); }
244
245auto http_server::wait_for_stop() -> void { tcp_server_->wait_for_stop(); }
246
247auto http_server::get(const std::string &pattern, http_handler handler)
248 -> void {
249 register_route(internal::http_method::HTTP_GET, pattern, std::move(handler));
250}
251
252auto http_server::post(const std::string &pattern, http_handler handler)
253 -> void {
254 register_route(internal::http_method::HTTP_POST, pattern, std::move(handler));
255}
256
257auto http_server::put(const std::string &pattern, http_handler handler)
258 -> void {
259 register_route(internal::http_method::HTTP_PUT, pattern, std::move(handler));
260}
261
262auto http_server::del(const std::string &pattern, http_handler handler)
263 -> void {
264 register_route(internal::http_method::HTTP_DELETE, pattern,
265 std::move(handler));
266}
267
268auto http_server::patch(const std::string &pattern, http_handler handler)
269 -> void {
270 register_route(internal::http_method::HTTP_PATCH, pattern,
271 std::move(handler));
272}
273
274auto http_server::head(const std::string &pattern, http_handler handler)
275 -> void {
276 register_route(internal::http_method::HTTP_HEAD, pattern, std::move(handler));
277}
278
279auto http_server::options(const std::string &pattern, http_handler handler)
280 -> void {
281 register_route(internal::http_method::HTTP_OPTIONS, pattern,
282 std::move(handler));
283}
284
286 not_found_handler_ = std::move(handler);
287}
288
290 error_handler_ = std::move(handler);
291}
292
294 error_handler handler) -> void {
295 std::lock_guard<std::mutex> lock(error_handlers_mutex_);
296 error_handlers_[code] = std::move(handler);
297}
298
300 std::lock_guard<std::mutex> lock(error_handlers_mutex_);
301 default_error_handler_ = std::move(handler);
302}
303
304auto http_server::set_request_timeout(std::chrono::milliseconds timeout)
305 -> void {
306 request_timeout_ = timeout;
307}
308
309auto http_server::set_json_error_responses(bool enable) -> void {
310 use_json_errors_ = enable;
311}
312
315 // Check for specific error handler
316 {
317 std::lock_guard<std::mutex> lock(error_handlers_mutex_);
318 auto it = error_handlers_.find(error.code);
319 if (it != error_handlers_.end()) {
320 return it->second(error);
321 }
322
323 // Check for default error handler
324 if (default_error_handler_) {
325 return default_error_handler_(error);
326 }
327 }
328
329 // Use built-in error response builder
330 if (use_json_errors_) {
332 }
334}
335
337 const std::string &pattern,
338 http_handler handler) -> void {
339 http_route route;
340 route.method = method;
341 route.pattern = pattern;
342 route.handler = std::move(handler);
343
344 // Convert pattern to regex
345 auto regex_str = pattern_to_regex(pattern, route.param_names);
346 route.regex_pattern = std::regex(regex_str);
347
348 std::lock_guard<std::mutex> lock(routes_mutex_);
349 routes_.push_back(std::move(route));
350}
351
353 internal::http_method method, const std::string &path,
354 std::map<std::string, std::string> &path_params) const
355 -> const http_route * {
356 // Note: routes_mutex_ is mutable, so we can lock it in const methods
357 // But for now, we'll const_cast to avoid making mutex mutable
358 auto &mutable_mutex = const_cast<std::mutex &>(routes_mutex_);
359 std::lock_guard<std::mutex> lock(mutable_mutex);
360
361 for (const auto &route : routes_) {
362 if (route.matches(method, path, path_params)) {
363 return &route;
364 }
365 }
366
367 return nullptr;
368}
369
370auto http_server::handle_request(const std::vector<uint8_t> &request_data)
371 -> std::vector<uint8_t> {
372 // Parse HTTP request
373 auto request_result = internal::http_parser::parse_request(request_data);
374 if (request_result.is_err()) {
375 // Failed to parse request - send 400 Bad Request
376 auto error_response = create_error_response(400, "Bad Request");
377 return internal::http_parser::serialize_response(error_response);
378 }
379
380 auto http_request = std::move(request_result.value());
381
382 // Create request context
384 ctx.request = std::move(http_request);
385
387
388 try {
389 // Find matching route
390 auto route =
391 find_route(ctx.request.method, ctx.request.uri, ctx.path_params);
392
393 if (route) {
394 // Execute handler
395 response = route->handler(ctx);
396 } else {
397 // No matching route - send 404
398 response = not_found_handler_(ctx);
399 }
400 } catch (const std::exception &e) {
401 // Handler threw exception - send 500
402 response = error_handler_(ctx);
403 } catch (...) {
404 // Unknown exception - send 500
405 response = error_handler_(ctx);
406 }
407
408 // Add standard headers if not present
409 if (!response.get_header("Content-Length")) {
410 response.set_header("Content-Length", std::to_string(response.body.size()));
411 }
412
413 if (!response.get_header("Server")) {
414 response.set_header("Server", "NetworkSystem-HTTP-Server/1.0");
415 }
416
417 // Serialize and return response
419}
420
422 const internal::http_request &http_request) -> internal::http_response {
423 // Create request context
425 ctx.request = http_request;
426
428
429 try {
430 // Find matching route
431 auto route =
432 find_route(ctx.request.method, ctx.request.uri, ctx.path_params);
433
434 if (route) {
435 // Execute handler
436 response = route->handler(ctx);
437 } else {
438 // No matching route - send 404
439 response = not_found_handler_(ctx);
440 }
441 } catch (const std::exception &e) {
442 // Handler threw exception - send 500
443 response = error_handler_(ctx);
444 } catch (...) {
445 // Unknown exception - send 500
446 response = error_handler_(ctx);
447 }
448
449 // Add standard headers if not present
450 if (!response.get_header("Content-Length")) {
451 response.set_header("Content-Length", std::to_string(response.body.size()));
452 }
453
454 if (!response.get_header("Server")) {
455 response.set_header("Server", "NetworkSystem-HTTP-Server/1.0");
456 }
457
458 // Add Connection header based on request if not already present
459 if (!response.get_header("Connection")) {
460 // HTTP/1.1 defaults to keep-alive, HTTP/1.0 defaults to close
461 if (http_request.version == internal::http_version::HTTP_1_0) {
462 auto conn = http_request.get_header("Connection");
463 if (conn && *conn == "keep-alive") {
464 response.set_header("Connection", "keep-alive");
465 } else {
466 response.set_header("Connection", "close");
467 }
468 } else // HTTP/1.1
469 {
470 auto conn = http_request.get_header("Connection");
471 if (conn && *conn == "close") {
472 response.set_header("Connection", "close");
473 }
474 // else: keep-alive is default for HTTP/1.1
475 }
476 }
477
478 // Apply compression if enabled and appropriate
479 apply_compression(http_request, response);
480
481 return response;
482}
483
485 const internal::http_request &request,
486 const internal::http_response &response) const -> bool {
487 // Check response Connection header first
488 auto response_conn = response.get_header("Connection");
489 if (response_conn) {
490 std::string conn_lower = *response_conn;
491 std::transform(conn_lower.begin(), conn_lower.end(), conn_lower.begin(),
492 [](unsigned char c) { return std::tolower(c); });
493 if (conn_lower == "close") {
494 return true;
495 }
496 if (conn_lower == "keep-alive") {
497 return false;
498 }
499 }
500
501 // Check request Connection header
502 auto request_conn = request.get_header("Connection");
503 if (request_conn) {
504 std::string conn_lower = *request_conn;
505 std::transform(conn_lower.begin(), conn_lower.end(), conn_lower.begin(),
506 [](unsigned char c) { return std::tolower(c); });
507 if (conn_lower == "close") {
508 return true;
509 }
510 }
511
512 // HTTP/1.0 defaults to close unless Connection: keep-alive
513 if (request.version == internal::http_version::HTTP_1_0) {
514 return true;
515 }
516
517 // HTTP/1.1 defaults to keep-alive
518 return false;
519}
520
522 const std::string &message)
524 // Create http_error from status code
526 static_cast<internal::http_error_code>(status_code), message);
527
528 return build_error_response(error);
529}
530
531auto http_server::pattern_to_regex(const std::string &pattern,
532 std::vector<std::string> &param_names)
533 -> std::string {
534 param_names.clear();
535
536 std::ostringstream regex_str;
537 regex_str << "^";
538
539 std::size_t pos = 0;
540 while (pos < pattern.length()) {
541 // Look for parameter placeholder (:param_name)
542 auto param_start = pattern.find(':', pos);
543
544 if (param_start == std::string::npos) {
545 // No more parameters, add remaining literal part
546 auto remaining = pattern.substr(pos);
547 // Escape special regex characters
548 for (char c : remaining) {
549 if (c == '.' || c == '*' || c == '+' || c == '?' || c == '[' ||
550 c == ']' || c == '(' || c == ')' || c == '{' || c == '}' ||
551 c == '^' || c == '$' || c == '|' || c == '\\') {
552 regex_str << '\\';
553 }
554 regex_str << c;
555 }
556 break;
557 }
558
559 // Add literal part before parameter
560 for (std::size_t i = pos; i < param_start; ++i) {
561 char c = pattern[i];
562 if (c == '.' || c == '*' || c == '+' || c == '?' || c == '[' ||
563 c == ']' || c == '(' || c == ')' || c == '{' || c == '}' ||
564 c == '^' || c == '$' || c == '|' || c == '\\') {
565 regex_str << '\\';
566 }
567 regex_str << c;
568 }
569
570 // Find end of parameter name
571 auto param_end = param_start + 1;
572 while (param_end < pattern.length() &&
573 (std::isalnum(pattern[param_end]) || pattern[param_end] == '_')) {
574 ++param_end;
575 }
576
577 // Extract parameter name
578 auto param_name =
579 pattern.substr(param_start + 1, param_end - param_start - 1);
580 param_names.push_back(param_name);
581
582 // Add regex capture group for parameter (matches anything except /)
583 regex_str << "([^/]+)";
584
585 pos = param_end;
586 }
587
588 regex_str << "$";
589
590 return regex_str.str();
591}
592
593auto http_server::set_compression_enabled(bool enable) -> void {
594 std::lock_guard<std::mutex> lock(compression_mutex_);
595 compression_enabled_ = enable;
596}
597
598auto http_server::set_compression_threshold(size_t threshold_bytes) -> void {
599 std::lock_guard<std::mutex> lock(compression_mutex_);
600 compression_threshold_ = threshold_bytes;
601}
602
604 const std::string &accept_encoding) const -> utils::compression_algorithm {
605 // Parse Accept-Encoding header value (e.g., "gzip, deflate, br")
606 // Priority: gzip > deflate > none
607 // Note: We only support gzip and deflate (not br/brotli)
608
609 // Convert to lowercase for case-insensitive comparison
610 std::string accept_lower = accept_encoding;
611 std::transform(accept_lower.begin(), accept_lower.end(), accept_lower.begin(),
612 [](unsigned char c) { return std::tolower(c); });
613
614 // Check for gzip first (highest priority)
615 if (accept_lower.find("gzip") != std::string::npos) {
617 }
618
619 // Check for deflate next
620 if (accept_lower.find("deflate") != std::string::npos) {
622 }
623
624 // No supported compression algorithm found
626}
627
629 internal::http_response &response) -> void {
630 // Check if compression is enabled and response size meets threshold
631 {
632 std::lock_guard<std::mutex> lock(compression_mutex_);
633 if (!compression_enabled_) {
634 return; // Compression disabled
635 }
636
637 if (response.body.size() < compression_threshold_) {
638 return; // Response too small to compress
639 }
640 }
641
642 // Get Accept-Encoding header from request
643 auto accept_encoding = request.get_header("Accept-Encoding");
644 if (!accept_encoding) {
645 return; // Client doesn't support compression
646 }
647
648 // Determine best compression algorithm
649 auto algorithm = choose_compression_algorithm(*accept_encoding);
650 if (algorithm == utils::compression_algorithm::none) {
651 return; // No supported compression algorithm
652 }
653
654 // Store original size for logging
655 const size_t original_size = response.body.size();
656
657 // Create compression pipeline for the selected algorithm
658 auto pipeline = std::make_shared<utils::compression_pipeline>(algorithm);
659
660 // Compress response body
661 auto compressed_result = pipeline->compress(response.body);
662
663 if (compressed_result.is_err()) {
664 // Compression failed - send uncompressed
665 // TODO: Add error logging when needed
666 return;
667 }
668
669 // Get compressed size before moving
670 const size_t compressed_size = compressed_result.value().size();
671
672 // Replace response body with compressed data
673 response.body = std::move(compressed_result.value());
674
675 // Set Content-Encoding header to indicate compression
676 if (algorithm == utils::compression_algorithm::gzip) {
677 response.set_header("Content-Encoding", "gzip");
678 } else if (algorithm == utils::compression_algorithm::deflate) {
679 response.set_header("Content-Encoding", "deflate");
680 }
681
682 // Update Content-Length header with new compressed size
683 response.set_header("Content-Length", std::to_string(response.body.size()));
684
685 // Suppress unused variable warnings
686 (void)original_size;
687 (void)compressed_size;
688}
689
690} // namespace kcenon::network::core
auto options(const std::string &pattern, http_handler handler) -> void
Register OPTIONS route handler.
auto start(unsigned short port) -> VoidResult
Start HTTP server on specified port.
auto get(const std::string &pattern, http_handler handler) -> void
Register GET route handler.
http_server(const std::string &server_id)
Construct HTTP server with server ID.
auto should_close_connection(const internal::http_request &request, const internal::http_response &response) const -> bool
Determine if connection should be closed after response.
auto set_not_found_handler(http_handler handler) -> void
Set custom 404 Not Found handler.
auto find_route(internal::http_method method, const std::string &path, std::map< std::string, std::string > &path_params) const -> const http_route *
Find matching route for request.
auto create_error_response(int status_code, const std::string &message) -> internal::http_response
Create default error response.
auto del(const std::string &pattern, http_handler handler) -> void
Register DELETE route handler.
static auto pattern_to_regex(const std::string &pattern, std::vector< std::string > &param_names) -> std::string
Convert route pattern to regex pattern.
~http_server()
Destructor - stops server if running.
auto set_json_error_responses(bool enable) -> void
Enable JSON format for error responses.
auto process_http_request(const internal::http_request &request) -> internal::http_response
Process HTTP request and generate response object.
auto set_request_timeout(std::chrono::milliseconds timeout) -> void
Set request timeout duration.
auto stop() -> VoidResult
Stop HTTP server.
auto set_compression_enabled(bool enable) -> void
Enable automatic response compression.
auto handle_request(const std::vector< uint8_t > &request_data) -> std::vector< uint8_t >
Handle incoming HTTP request.
auto register_route(internal::http_method method, const std::string &pattern, http_handler handler) -> void
Register route with method and pattern.
auto wait_for_stop() -> void
Wait for server to stop (blocking)
auto post(const std::string &pattern, http_handler handler) -> void
Register POST route handler.
auto put(const std::string &pattern, http_handler handler) -> void
Register PUT route handler.
auto build_error_response(const internal::http_error &error) -> internal::http_response
Build error response using registered handlers.
auto set_error_handler(http_handler handler) -> void
Set custom 500 Internal Server Error handler.
auto choose_compression_algorithm(const std::string &accept_encoding) const -> utils::compression_algorithm
Determine compression algorithm from Accept-Encoding header.
auto head(const std::string &pattern, http_handler handler) -> void
Register HEAD route handler.
auto apply_compression(const internal::http_request &request, internal::http_response &response) -> void
Apply compression to response if appropriate.
auto set_compression_threshold(size_t threshold_bytes) -> void
Set minimum response size for compression.
auto set_default_error_handler(error_handler handler) -> void
Set default error handler for all unhandled error codes.
std::shared_ptr< messaging_server > tcp_server_
auto patch(const std::string &pattern, http_handler handler) -> void
Register PATCH route handler.
A server class that manages incoming TCP connections, creating messaging_session instances for each a...
Definition tcp.cppm:237
static auto build_json_error(const http_error &error) -> http_response
Build JSON format error response (RFC 7807)
static auto build_html_error(const http_error &error) -> http_response
Build HTML format error response.
static auto make_error(http_error_code code, const std::string &detail="", const std::string &request_id="") -> http_error
Create http_error from error code.
static auto parse_request(const std::vector< uint8_t > &data) -> Result< http_request >
Parse HTTP request from raw bytes.
static auto serialize_response(const http_response &response) -> std::vector< uint8_t >
Serialize HTTP response to raw bytes.
Messaging session managing bidirectional message exchange.
std::function< internal::http_response(const http_request_context &ctx)> http_handler
Handler function for HTTP requests.
std::function< internal::http_response(const internal::http_error &error)> error_handler
Handler function for HTTP errors.
http_error_code
Standard HTTP error codes (RFC 7231)
Definition http_error.h:20
http_method
HTTP request methods (verbs)
Definition http_types.h:24
compression_algorithm
Supported compression algorithms.
auto append(const std::vector< uint8_t > &chunk) -> bool
Append new data chunk to buffer.
static auto parse_content_length(const std::vector< uint8_t > &data, std::size_t headers_end) -> std::size_t
Parse Content-Length from headers.
auto is_complete() const -> bool
Check if complete HTTP request has been received.
static constexpr std::size_t MAX_REQUEST_SIZE
Definition http_server.h:34
static auto find_header_end(const std::vector< uint8_t > &data) -> std::size_t
Find end of HTTP headers (\r\n\r\n marker)
Context for an HTTP request with parsed components.
Definition http_server.h:71
std::map< std::string, std::string > path_params
Definition http_server.h:73
Route definition with pattern matching and handler.
auto matches(internal::http_method method, const std::string &path, std::map< std::string, std::string > &path_params) const -> bool
Check if request matches this route.
std::vector< std::string > param_names
internal::http_method method
Structured HTTP error information.
Definition http_error.h:78
Represents an HTTP request message.
Definition http_types.h:112
Represents an HTTP response message.
Definition http_types.h:162
auto set_header(const std::string &name, const std::string &value) -> void
Set a header value.
auto get_header(const std::string &name) const -> std::optional< std::string >
Get the value of a header (case-insensitive)