Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions app/src/sm_at_httpc.c
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,15 @@ static void http_send_data(struct http_request *req, const uint8_t *data, int le
data_send(req->pipe, data, len);
}

/*
* Detect HTTP/1.1 chunked transfer EOF marker in a body data buffer.
* The final chunk is always "0\r\n\r\n" (zero-length chunk, RFC 9112 §7.1.1).
*/
static bool chunked_eof(const uint8_t *data, int len)
{
return len >= 5 && memcmp(data + len - 5, "0\r\n\r\n", 5) == 0;
Comment thread
trantanen marked this conversation as resolved.
}

/* Parse HTTP status code from response buffer */
static int parse_http_status_code(const char *buf, int *status_code)
{
Expand Down Expand Up @@ -616,12 +625,14 @@ static bool http_headers_complete(struct http_request *req, char *header_end,
}

/*
* All body bytes arrived piggybacked with the headers and
* content-length is satisfied. With keep-alive connections
* there is no subsequent EOF to trigger the completion check
* in the RECEIVING_BODY path, so finish here instead.
* Finish early when all body data is already in hand (piggybacked):
* either content-length is satisfied, or chunked terminator was received.
* With keep-alive connections there is no subsequent EOF to trigger the
* completion check in the RECEIVING_BODY path.
*/
if (req->content_length >= 0 && req->bytes_sent >= req->content_length) {
if ((req->content_length >= 0 && req->bytes_sent >= req->content_length) ||
(req->content_length < 0 &&
chunked_eof(req->recv_buf + body_offset, body_len))) {
http_finish_request(req);
return true;
}
Expand Down Expand Up @@ -697,11 +708,31 @@ static void http_process_request(struct http_request *req, uint8_t events)
/* POLLHUP without POLLIN before headers are received is an error:
* the server closed the connection before sending a valid response.
*/
if ((events & NRF_POLLHUP) && !(events & NRF_POLLIN) &&
!req->headers_complete) {
LOG_ERR("HTTP %d: Connection closed before headers (POLLHUP)", req->fd);
http_fail_request(req);
return;
if (events & NRF_POLLHUP) {
if (!req->headers_complete) {
/* Server closed before headers arrived */
LOG_ERR("HTTP %d: Connection closed before headers (POLLHUP)",
req->fd);
http_fail_request(req);
return;
}
if (!(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)
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.
*/
}
}

/* Handle POLLIN */
Expand Down Expand Up @@ -786,14 +817,18 @@ static void http_process_request(struct http_request *req, uint8_t events)
xapoll_stop(sock);
return;
}
bool 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, or if the connection
* is already closing (POLLHUP co-fired) — no point re-arming
* POLLIN on a closing socket; nrf_recv would return EAGAIN.
/* Finish if:
* - content-length satisfied (known-length transfer)
* - chunked terminator "0\r\n\r\n" just received
* - POLLHUP co-fired (connection closing)
*/
if ((req->content_length > 0 &&
if (body_done ||
(req->content_length > 0 &&
req->bytes_sent >= req->content_length) ||
(events & NRF_POLLHUP)) {
http_finish_request(req);
Expand Down
43 changes: 39 additions & 4 deletions doc/app/at_httpc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Unsolicited notification
* The ``<status_code>`` parameter is an integer.
It contains the HTTP status code returned by the server.
* The ``<content_length>`` parameter is an integer.
It contains the value of the ``Content-Length`` response header, or ``-1`` when no such header is present.
It contains the value of the ``Content-Length`` response header, or ``-1`` when the server uses chunked transfer encoding or does not provide a content length.

``#XHTTPCDATA`` is emitted in automatic mode for each received body chunk::

Expand All @@ -124,6 +124,14 @@ The notification line is terminated with ``\r\n`` and the raw body bytes follow
* The ``<length>`` parameter is an integer.
It contains the number of body bytes in this chunk.

.. note::

When the server uses ``Transfer-Encoding: chunked``, the body bytes delivered via ``#XHTTPCDATA`` include the raw chunked framing.
Each chunk consists of a size line (hexadecimal length followed by ``\r\n``), the chunk data, and a trailing ``\r\n``.
The final zero-length chunk ``0\r\n\r\n`` is also forwarded.
The host strips this framing to recover the original body content.
The ``<total_bytes>`` field in ``#XHTTPCSTAT`` reflects the raw wire byte count, including chunked framing, not the decoded body length.

``#XHTTPCSTAT`` is emitted when the request completes, fails, or is cancelled::

#XHTTPCSTAT: <handle>,<status_code>,<total_bytes>
Expand All @@ -134,6 +142,7 @@ The notification line is terminated with ``\r\n`` and the raw body bytes follow
It contains the HTTP status code on success, or ``-1`` on failure, cancel, or timeout.
* The ``<total_bytes>`` parameter is an integer.
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.

.. note::
Expand Down Expand Up @@ -235,6 +244,28 @@ HTTP HEAD (no body — ``#XHTTPCSTAT`` follows immediately after ``#XHTTPCHEAD``

#XHTTPCSTAT: 0,200,0

HTTP POST with chunked response (``content_length=-1``):

::

AT#XHTTPCREQ=0,<url>,1,1,1024
#XHTTPCREQ: 0
OK
<1024 bytes payload>
#XDATAMODE: 0

#XHTTPCHEAD: 0,200,-1

#XHTTPCDATA: 0,0,1132
460\r\n{"args":{},"data":"<1024 bytes>","url":"..."}\r\n0\r\n\r\n

#XHTTPCSTAT: 0,200,1132

.. note::

The 1132 raw bytes break down as chunked framing: ``460\r\n`` (chunk-size 1120 decimal in hex, 5 bytes), 1120 bytes of JSON body, ``\r\n`` (chunk trailer, 2 bytes), and ``0\r\n\r\n`` (final zero-length chunk, 5 bytes).
Strip this framing to recover the 1120-byte JSON body.

Test command
------------

Expand Down Expand Up @@ -313,9 +344,13 @@ When the socket buffer is temporarily empty (EAGAIN)::
It contains the number of body bytes delivered in this pull.
A value of ``0`` means the socket buffer is currently empty.

When all body bytes have been delivered, ``#XHTTPCSTAT`` is sent as a URC after
the final ``OK``. This happens either when the server closes the connection, or
when the ``Content-Length`` bytes have all been forwarded.
When all body bytes have been delivered, ``#XHTTPCSTAT`` is sent as a URC after the final ``OK``.
This happens either when the server closes the connection, when the ``Content-Length`` bytes have all been forwarded, or when the chunked transfer ``0\r\n\r\n`` terminator is received.

.. note::

When the server uses ``Transfer-Encoding: chunked`` (``content_length=-1`` in ``#XHTTPCHEAD``), the bytes returned by ``#XHTTPCDATA`` include raw chunked framing.
See the note under ``#XHTTPCDATA`` in the ``AT#XHTTPCREQ`` section.

.. note::

Expand Down
Loading