From fd4be41b9c1e26632b09d2d1093ee2d30301d946 Mon Sep 17 00:00:00 2001 From: Juha Ylinen Date: Wed, 13 May 2026 09:19:59 +0300 Subject: [PATCH 1/2] app: http: Handle "Connection: close" header in HTTP response Add field to #XHTTPCSTAT URC to indicate when the server signals connection closure via the "Connection: close" header. XHTTPCSTAT: ,,, When is set, the host must close and reopen the socket before sending the next request. Signed-off-by: Juha Ylinen --- app/src/sm_at_httpc.c | 47 ++++++++++++++++++++++++++++++++++--------- doc/app/at_httpc.rst | 33 +++++++++++++++++++----------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/app/src/sm_at_httpc.c b/app/src/sm_at_httpc.c index 478e8477..2c4c8e9d 100644 --- a/app/src/sm_at_httpc.c +++ b/app/src/sm_at_httpc.c @@ -82,6 +82,7 @@ struct http_request { struct modem_pipe *pipe; /* AT pipe that created this request */ bool manual_mode; /* Manual mode: body not auto-received, host pulls chunks */ int bytes_sent; /* Response-body bytes sent to the host */ + bool connection_close; /* Server sent "Connection: close" header */ }; static const char * const http_method_str[] = { @@ -105,6 +106,7 @@ static bool http_headers_complete(struct http_request *req, char *header_end, struct sm_socket *sock, bool hup); static int parse_http_status_code(const char *buf, int *status_code); static int parse_content_length(const char *buf, const char *header_end, int *length); +static bool parse_connection_close(const char *buf, const char *header_end); static void http_timeout_work_fn(struct k_work *work); static K_MUTEX_DEFINE(http_mutex); @@ -433,20 +435,22 @@ static void http_close_request(struct http_request *req) /* Send error via XHTTPCSTAT with -1 status code */ static void http_send_error(struct http_request *req) { - urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,-1,%d\r\n", req->fd, req->total_received); + urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,-1,%d,%d\r\n", req->fd, + req->total_received, (int)req->connection_close); } /* Send status URC */ static void http_send_status(struct http_request *req) { - urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,%d,%d\r\n", req->fd, req->status_code, - req->total_received); + urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,%d,%d,%d\r\n", req->fd, req->status_code, + req->total_received, (int)req->connection_close); } /* Send cancel status URC with bytes already delivered to host */ static void http_send_cancel_status(struct http_request *req) { - urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,-1,%d\r\n", req->fd, req->bytes_sent); + urc_send_to(req->pipe, "\r\n#XHTTPCSTAT: %d,-1,%d,%d\r\n", req->fd, + req->bytes_sent, (int)req->connection_close); } /* Send error and close request */ @@ -511,6 +515,19 @@ static int parse_http_status_code(const char *buf, int *status_code) return 0; } +/* Parse Connection: close header from response buffer */ +static bool parse_connection_close(const char *buf, const char *header_end) +{ + const char *p; + + p = strstr(buf, "Connection: close"); + if (!p) { + p = strstr(buf, "connection: close"); + } + + return p != NULL && p < header_end; +} + /* Parse Content-Length header from response buffer */ static int parse_content_length(const char *buf, const char *header_end, int *length) { @@ -563,6 +580,11 @@ static bool http_headers_complete(struct http_request *req, char *header_end, LOG_DBG("HTTP %d: No Content-Length header", req->fd); } + req->connection_close = parse_connection_close((char *)req->recv_buf, header_end); + if (req->connection_close) { + LOG_INF("HTTP %d: Server sent Connection: close", req->fd); + } + req->headers_complete = true; req->state = HTTP_STATE_RECEIVING_BODY; @@ -707,22 +729,26 @@ static void http_process_request(struct http_request *req, uint8_t events) case HTTP_STATE_RECEIVING_BODY: /* POLLHUP without POLLIN before headers are received is an error: * the server closed the connection before sending a valid response. + * When POLLIN co-fires, data may still be in the socket buffer (e.g. + * a server with Connection: close that sends headers and closes + * simultaneously); fall through to the POLLIN handler to drain it. */ if (events & NRF_POLLHUP) { - if (!req->headers_complete) { - /* Server closed before headers arrived */ + if (!req->headers_complete && !(events & NRF_POLLIN)) { + /* Server closed before headers arrived and no data to read */ LOG_ERR("HTTP %d: Connection closed before headers (POLLHUP)", req->fd); http_fail_request(req); return; } - if (!(events & NRF_POLLIN)) { + if (req->headers_complete && !(events & NRF_POLLIN)) { /* POLLHUP alone during body reception: server closed cleanly - * after all data. Treat as EOF. + * after all data. Treat as EOF. */ if (req->state == HTTP_STATE_RECEIVING_BODY) { if (req->content_length > 0 && - req->total_received < req->content_length) + req->total_received < req->content_length && + !req->connection_close) LOG_WRN("HTTP %d: Incomplete - %d/%d bytes", req->fd, req->total_received, req->content_length); @@ -768,7 +794,8 @@ static void http_process_request(struct http_request *req, uint8_t events) /* Check if we received all expected data */ if (req->content_length > 0 && - req->total_received < req->content_length) { + req->total_received < req->content_length && + !req->connection_close) { LOG_WRN("HTTP %d: Incomplete transfer - received %d/%d " "bytes", req->fd, req->total_received, diff --git a/doc/app/at_httpc.rst b/doc/app/at_httpc.rst index 58d7c3fe..c44e3646 100644 --- a/doc/app/at_httpc.rst +++ b/doc/app/at_httpc.rst @@ -134,7 +134,7 @@ The notification line is terminated with ``\r\n`` and the raw body bytes follow ``#XHTTPCSTAT`` is emitted when the request completes, fails, or is cancelled:: - #XHTTPCSTAT: ,, + #XHTTPCSTAT: ,,, * The ```` parameter is an integer. It identifies the socket. @@ -144,6 +144,10 @@ The notification line is terminated with ``\r\n`` and the raw body bytes follow On successful completion, failure, or timeout, it contains the total number of response body bytes received by the HTTP client. For chunked transfer encoding this includes the raw framing bytes (chunk-size lines, ``\r\n`` separators, and the final ``0\r\n\r\n`` terminator). On cancel (``status_code=-1`` from ``AT#XHTTPCCANCEL``), it contains the number of response body bytes already delivered to the host. +* The ```` parameter is an integer. + It is ``1`` when the server includes a ``Connection: close`` header in its response, indicating that the TCP connection will be closed after this response. + It is ``0`` otherwise (keep-alive connection). + When ```` is ``1``, the host must close the socket with ``AT#XCLOSE`` and open a new connection before issuing the next request. .. note:: @@ -169,7 +173,7 @@ HTTP GET (automatic mode): #XHTTPCDATA: 0,0,261 <261 bytes> - #XHTTPCSTAT: 0,200,261 + #XHTTPCSTAT: 0,200,261,0 HTTP GET (manual mode): @@ -186,7 +190,7 @@ HTTP GET (manual mode): <261 bytes> OK - #XHTTPCSTAT: 0,200,261 + #XHTTPCSTAT: 0,200,261,0 HTTP POST with JSON body and custom header: @@ -203,10 +207,12 @@ HTTP POST with JSON body and custom header: #XHTTPCDATA: 0,0,432 <432 bytes> - #XHTTPCSTAT: 0,200,432 + #XHTTPCSTAT: 0,200,432,0 HTTP GET with Range header (``body_len=0`` is required as a placeholder when headers follow a GET): +In this example the server returns ``connection_close=1``, so the host must close and reopen the socket before the next request. + :: AT#XHTTPCREQ=0,,0,1,0,"Range: bytes=0-127" @@ -218,7 +224,7 @@ HTTP GET with Range header (``body_len=0`` is required as a placeholder when hea #XHTTPCDATA: 0,0,128 <128 bytes> - #XHTTPCSTAT: 0,206,128 + #XHTTPCSTAT: 0,206,128,0 AT#XHTTPCREQ=0,,0,1,0,"Range: bytes=128-255" #XHTTPCREQ: 0 @@ -229,8 +235,11 @@ HTTP GET with Range header (``body_len=0`` is required as a placeholder when hea #XHTTPCDATA: 0,0,128 <128 bytes> - #XHTTPCSTAT: 0,206,128 + #XHTTPCSTAT: 0,206,128,1 + AT#XCLOSE=0 + #XCLOSE: 0,0 + OK HTTP HEAD (no body — ``#XHTTPCSTAT`` follows immediately after ``#XHTTPCHEAD``): @@ -242,7 +251,7 @@ HTTP HEAD (no body — ``#XHTTPCSTAT`` follows immediately after ``#XHTTPCHEAD`` #XHTTPCHEAD: 0,200,261 - #XHTTPCSTAT: 0,200,0 + #XHTTPCSTAT: 0,200,0,0 HTTP POST with chunked response (``content_length=-1``): @@ -259,7 +268,7 @@ HTTP POST with chunked response (``content_length=-1``): #XHTTPCDATA: 0,0,1132 460\r\n{"args":{},"data":"<1024 bytes>","url":"..."}\r\n0\r\n\r\n - #XHTTPCSTAT: 0,200,1132 + #XHTTPCSTAT: 0,200,1132,0 .. note:: @@ -372,7 +381,7 @@ Example <256 bytes> OK - #XHTTPCSTAT: 0,200,512 + #XHTTPCSTAT: 0,200,512,0 Test command ------------ @@ -412,7 +421,7 @@ The timer resets each time data is sent or received: * Receiving response headers or body bytes. * Pulling a body chunk in manual mode (``AT#XHTTPCDATA``). -If no such activity occurs within the configured window, the request is aborted and ``#XHTTPCSTAT: ,-1,`` is emitted. +If no such activity occurs within the configured window, the request is aborted and ``#XHTTPCSTAT: ,-1,,`` is emitted. The timeout is enforced by a background timer that fires independently of normal socket poll events, so a server that stalls silently (no TCP RST or FIN) is also detected. HTTP request cancel #XHTTPCCANCEL @@ -435,7 +444,7 @@ Syntax * The ```` parameter is an integer. It identifies the socket of the request to cancel. -An unsolicited ``#XHTTPCSTAT: ,-1,`` notification is emitted after cancellation, where ```` is the number of response body bytes already delivered to the host. +An unsolicited ``#XHTTPCSTAT: ,-1,,`` notification is emitted after cancellation, where ```` is the number of response body bytes already delivered to the host. Example ~~~~~~~ @@ -444,7 +453,7 @@ Example AT#XHTTPCCANCEL=0 OK - #XHTTPCSTAT: 0,-1,0 + #XHTTPCSTAT: 0,-1,0,0 Test command ------------ From baf8da87cabd1630aa0e3c955afd8fd69a3c7b1d Mon Sep 17 00:00:00 2001 From: Juha Ylinen Date: Mon, 18 May 2026 10:55:06 +0300 Subject: [PATCH 2/2] app: http: Simplify http_process_request receive cases Split HTTP_STATE_RECEIVING_HEADERS and HTTP_STATE_RECEIVING_BODY to separate switch cases. Signed-off-by: Juha Ylinen --- app/src/sm_at_httpc.c | 289 +++++++++++++++++++++++------------------- 1 file changed, 159 insertions(+), 130 deletions(-) diff --git a/app/src/sm_at_httpc.c b/app/src/sm_at_httpc.c index 2c4c8e9d..a67da6a6 100644 --- a/app/src/sm_at_httpc.c +++ b/app/src/sm_at_httpc.c @@ -104,6 +104,12 @@ static void http_finish_request(struct http_request *req); static int http_start_request(struct http_request *req); static bool http_headers_complete(struct http_request *req, char *header_end, struct sm_socket *sock, bool hup); +static void http_warn_incomplete_transfer(const struct http_request *req); +static int http_recv_read(struct http_request *req, struct sm_socket *sock); +static void http_process_recv_headers(struct http_request *req, struct sm_socket *sock, + uint8_t events); +static void http_process_recv_body(struct http_request *req, struct sm_socket *sock, + uint8_t events); static int parse_http_status_code(const char *buf, int *status_code); static int parse_content_length(const char *buf, const char *header_end, int *length); static bool parse_connection_close(const char *buf, const char *header_end); @@ -635,8 +641,9 @@ static bool http_headers_complete(struct http_request *req, char *header_end, LOG_DBG("HTTP %d: Headers complete, status %d", req->fd, req->status_code); /* - * If POLLHUP co-fired the connection is already closing; finish now - * rather than re-arming POLLIN on a socket that will never fire again. + * If POLLHUP arrived together with POLLIN, the connection is already + * closing; finish now rather than re-arming POLLIN on a socket that + * will never fire again. */ if (hup) { if (req->content_length > 0 && req->bytes_sent < req->content_length) @@ -662,6 +669,112 @@ static bool http_headers_complete(struct http_request *req, char *header_end, return false; } +static void http_warn_incomplete_transfer(const struct http_request *req) +{ + if (req->content_length > 0 && + req->total_received < req->content_length && + !req->connection_close) { + LOG_WRN("HTTP %d: Incomplete transfer - received %d/%d bytes", + req->fd, req->total_received, req->content_length); + } +} + +/* + * Read from the socket into recv_buf. On success, updates recv_buf_len, + * total_received, and the idle timeout. + * + * Returns: >0 bytes read + * 0 EOF (connection closed) + * -1 EAGAIN (POLLIN re-armed) + * -errno other recv error (caller should fail the request) + */ +static int http_recv_read(struct http_request *req, struct sm_socket *sock) +{ + int ret; + + ret = nrf_recv(req->fd, req->recv_buf + req->recv_buf_len, + HTTP_RECV_BUF_SIZE - req->recv_buf_len - 1, NRF_MSG_DONTWAIT); + + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + set_xapoll_events(sock, NRF_POLLIN); + return -1; + } + if (errno == ETIMEDOUT) { + LOG_ERR("Recv timed out"); + return -ETIMEDOUT; + } + LOG_ERR("Recv failed: %d", errno); + return -errno; + } + + if (ret == 0) { + return 0; + } + + req->recv_buf_len += ret; + req->recv_buf[req->recv_buf_len] = '\0'; + req->total_received += ret; + req->timeout_timestamp = k_uptime_get() + HTTP_RESPONSE_TIMEOUT_MS; + + return ret; +} + +static void http_process_recv_headers(struct http_request *req, struct sm_socket *sock, + uint8_t events) +{ + char *header_end = strstr((char *)req->recv_buf, "\r\n\r\n"); + + if (header_end) { + if (http_headers_complete(req, header_end, sock, events & NRF_POLLHUP)) { + return; + } + req->need_rearm_pollin = true; + return; + } + + if (req->recv_buf_len >= HTTP_RECV_BUF_SIZE - 1) { + LOG_ERR("HTTP headers too large"); + http_fail_request(req); + return; + } + + req->need_rearm_pollin = true; +} + +static void http_process_recv_body(struct http_request *req, struct sm_socket *sock, + uint8_t events) +{ + bool body_done; + + if (req->manual_mode) { + /* + * POLLIN fired in body state after xapoll_stop (race). + * nrf_recv already consumed bytes from the socket buffer + * into recv_buf and incremented total_received. Keep + * recv_buf intact so the host can pull it; do NOT reset + * recv_buf_len or the data is silently lost. + */ + xapoll_stop(sock); + return; + } + + body_done = chunked_eof(req->recv_buf, req->recv_buf_len); + + http_send_data(req, req->recv_buf, req->recv_buf_len); + req->recv_buf_len = 0; + + /* Finish if content-length satisfied, chunked EOF, or connection closing. */ + if (body_done || + (req->content_length > 0 && req->bytes_sent >= req->content_length) || + (events & NRF_POLLHUP)) { + http_finish_request(req); + return; + } + + req->need_rearm_pollin = true; +} + /* Process HTTP request state machine (event-driven via XAPOLL) */ static void http_process_request(struct http_request *req, uint8_t events) { @@ -726,146 +839,62 @@ static void http_process_request(struct http_request *req, uint8_t events) break; case HTTP_STATE_RECEIVING_HEADERS: - case HTTP_STATE_RECEIVING_BODY: - /* POLLHUP without POLLIN before headers are received is an error: - * the server closed the connection before sending a valid response. - * When POLLIN co-fires, data may still be in the socket buffer (e.g. - * a server with Connection: close that sends headers and closes - * simultaneously); fall through to the POLLIN handler to drain it. + /* + * POLLHUP without POLLIN: closed before any response. When POLLIN + * arrives at the same time, drain the socket buffer (e.g. Connection: close). */ - if (events & NRF_POLLHUP) { - if (!req->headers_complete && !(events & NRF_POLLIN)) { - /* Server closed before headers arrived and no data to read */ - LOG_ERR("HTTP %d: Connection closed before headers (POLLHUP)", - req->fd); - http_fail_request(req); - return; - } - if (req->headers_complete && !(events & NRF_POLLIN)) { - /* POLLHUP alone during body reception: server closed cleanly - * after all data. Treat as EOF. - */ - if (req->state == HTTP_STATE_RECEIVING_BODY) { - if (req->content_length > 0 && - req->total_received < req->content_length && - !req->connection_close) - LOG_WRN("HTTP %d: Incomplete - %d/%d bytes", - req->fd, req->total_received, - req->content_length); - http_finish_request(req); - return; - } - /* In RECEIVING_HEADERS, fall through to the POLLIN - * handler which may drain remaining bytes. - */ - } + if ((events & NRF_POLLHUP) && !(events & NRF_POLLIN)) { + LOG_ERR("HTTP %d: Connection closed before headers (POLLHUP)", + req->fd); + http_fail_request(req); + return; } - /* Handle POLLIN */ - if (events & (NRF_POLLIN)) { - ret = nrf_recv(req->fd, req->recv_buf + req->recv_buf_len, - HTTP_RECV_BUF_SIZE - req->recv_buf_len - 1, - NRF_MSG_DONTWAIT); + if (!(events & NRF_POLLIN)) { + return; + } - if (ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - /* Socket buffer empty - re-arm POLLIN to wait for more data - */ - set_xapoll_events(sock, NRF_POLLIN); - return; - } - if (errno == ETIMEDOUT) { - LOG_ERR("Recv timed out"); - http_fail_request(req); - return; - } - LOG_ERR("Recv failed: %d", errno); + ret = http_recv_read(req, sock); + if (ret < 0) { + if (ret != -1) { http_fail_request(req); - return; - } - - if (ret == 0) { - /* Connection closed by server (EOF) */ - if (!req->headers_complete) { - /* Closed before headers arrived */ - http_fail_request(req); - return; - } - - /* Check if we received all expected data */ - if (req->content_length > 0 && - req->total_received < req->content_length && - !req->connection_close) { - LOG_WRN("HTTP %d: Incomplete transfer - received %d/%d " - "bytes", - req->fd, req->total_received, - req->content_length); - } - - http_finish_request(req); - return; } + return; + } + if (ret == 0) { + http_fail_request(req); + return; + } - /* Data received - update idle timeout */ - req->recv_buf_len += ret; - req->recv_buf[req->recv_buf_len] = '\0'; - req->total_received += ret; - req->timeout_timestamp = k_uptime_get() + HTTP_RESPONSE_TIMEOUT_MS; - - if (req->state == HTTP_STATE_RECEIVING_HEADERS) { - /* Look for end of headers */ - char *header_end = strstr((char *)req->recv_buf, "\r\n\r\n"); - - if (header_end) { - if (http_headers_complete(req, header_end, sock, - events & NRF_POLLHUP)) { - return; - } - req->need_rearm_pollin = true; - return; - } + http_process_recv_headers(req, sock, events); + break; - /* Check if buffer is full */ - if (req->recv_buf_len >= HTTP_RECV_BUF_SIZE - 1) { - LOG_ERR("HTTP headers too large"); - http_fail_request(req); - return; - } - } else { - /* Receiving body */ - if (req->manual_mode) { - /* - * POLLIN fired in body state after xapoll_stop (race). - * nrf_recv already consumed bytes from the socket buffer - * into recv_buf and incremented total_received. Keep - * recv_buf intact so the host can pull it; do NOT reset - * recv_buf_len or the data is silently lost. - */ - xapoll_stop(sock); - return; - } - bool body_done = chunked_eof(req->recv_buf, req->recv_buf_len); + case HTTP_STATE_RECEIVING_BODY: + /* POLLHUP alone: server closed cleanly after sending body. */ + if ((events & NRF_POLLHUP) && !(events & NRF_POLLIN)) { + http_warn_incomplete_transfer(req); + http_finish_request(req); + return; + } - http_send_data(req, req->recv_buf, req->recv_buf_len); - req->recv_buf_len = 0; + if (!(events & NRF_POLLIN)) { + return; + } - /* Finish if: - * - content-length satisfied (known-length transfer) - * - chunked terminator "0\r\n\r\n" just received - * - POLLHUP co-fired (connection closing) - */ - if (body_done || - (req->content_length > 0 && - req->bytes_sent >= req->content_length) || - (events & NRF_POLLHUP)) { - http_finish_request(req); - return; - } + ret = http_recv_read(req, sock); + if (ret < 0) { + if (ret != -1) { + http_fail_request(req); } - - /* Set flag for socket layer to re-arm POLLIN for continuous reception */ - req->need_rearm_pollin = true; + return; + } + if (ret == 0) { + http_warn_incomplete_transfer(req); + http_finish_request(req); + return; } + + http_process_recv_body(req, sock, events); break; case HTTP_STATE_IDLE: