ncurl_session() already has a different jobThis note turns nanonext issue #329 into a small proposed API, states what it gives Rho, and separates public NNG facilities from implementation details that should remain private to NNG.
The proposed synchronous response head is enough for Rho to consume SSE once a
request is open. Rho receives the status and headers before the response body
ends, then each recv_aio() supplies another raw body fragment. A zero-length
raw vector means that the body has ended.
It does not make opening the request asynchronous. DNS lookup, TCP and TLS connection, request transmission, and receipt of the response head occupy the R process that calls the constructor. Rho can return a task around that call, but the task does not make the call cancellable or move it to another process.
The clean initial nanonext API is therefore useful, but Rho must describe it accurately and retain an HTTP implementation with asynchronous opening.
ncurl_session() already has a different jobncurl_session() is an existing exported function. Its documentation defines a
reusable connection on which repeated transact() calls return complete
responses (R wrapper and documentation).
The C constructor connects and waits before returning
(connection setup);
transact() then waits for a complete HTTP transaction
(complete transaction).
An incremental response is different:
| Existing session | Proposed response stream |
|---|---|
| reusable connection | one response body |
repeated transact() calls |
repeated recv_aio() calls |
| each call returns the complete body | each call returns currently available bytes |
| transaction result contains status, headers, and body | stream carries status and headers; body arrives later |
The existing function should remain unchanged. “Like ncurl_session()” is best
read as “a synchronous constructor with the same request conventions and owned
cleanup,” not as a request to overload the session class.
A separate constructor keeps the two lifecycles clear:
stream <- ncurl_stream(
url,
method = NULL,
headers = NULL,
data = NULL,
response = NULL,
timeout = NULL,
tls = NULL,
buffer = 65536L
)
It returns either an errorValue or a stream recognized by ordinary nanonext
operations. The response head is attached to that stream:
stream$status
stream$headers
receiving <- recv_aio(stream, mode = "raw", timeout = 30000L)
bytes <- receiving[]
if (!length(bytes)) {
# The response body has ended.
}
close(stream)
The class may be a nanoStream or an HTTP subtype, but these behaviors matter
more than the class spelling:
recv_aio(stream, mode = "raw") is the only body-receive operation;stop_aio() interrupts a pending receive;close() interrupts a pending receive and follows the existing nanonext
stream convention on a later close;raw(0) means end-of-stream and nothing else;raw(0).There is no convert argument. A network read can split an SSE line or a UTF-8
character, so conversion belongs after the caller has assembled complete
application data. The existing response argument should be considered for
consistency with the ncurl family: Rho would request response = TRUE, while a
caller could request selected headers or none.
The opening timeout should cover the whole call through receipt of the final
response head. A body-receive timeout is supplied separately to recv_aio().
The Rho fork added ncurl_stream_aio() and ncurl_stream_recv()
(public R additions),
plus is_ncurl_stream()
(validator).
It proves that nanonext can:
Its lifecycle tests cover an early SSE event, a later event, end-of-stream, repeated end-of-stream, opening cancellation, receive cancellation, concurrent receive rejection, and timeout (fork tests).
For a response framed by connection close, the fork treats both NNG’s ordinary closed value and its connection-shutdown value as end-of-body. It does not apply that rule to fixed-length or chunked responses: a peer closing those before their declared framing completes remains a transport error (receive completion).
The fork’s public shape is not the desired final shape. Its opening resolves to
list(status, headers, stream), and its special receive resolves to
list(data, complete)
(native result construction).
The stream has only the ncurlStream class
(object construction),
so ordinary recv_aio() does not recognize it. The maintainer’s proposal removes
those extra result shapes and body methods.
An ordinary nanonext stream currently contains an nng_stream *
(native stream structure).
recv_aio() extracts that pointer and calls nng_stream_recv() directly
(stream receive path).
An HTTP response body instead owns an nng_http_conn * and must call the HTTP
read operation.
Changing only the R class would therefore be unsafe. Nanonext needs a small
internal distinction between its ordinary byte stream and an HTTP-body stream.
Its existing recv_aio() and close() implementations can select the correct
native operation from that distinction. Sending on an HTTP-body stream should
return the ordinary unsupported-operation value because the stream is read-only.
The fork already has a nanonext-owned HTTP state object for fixed-length, chunked, connection-ended, and bodyless responses (state definitions). That state machine is the useful implementation core to adapt to the ordinary stream interface.
Inspecting NNG source is warranted for understanding and tests. Depending on its private symbols is not warranted.
NNG 1.12 already exposes the required calls publicly: connect a client, write a request, read a response head, read raw bytes, and close the connection (public HTTP connection API, public client API). The fork uses those public calls for opening and reading (open sequence, body read).
Reading private source answers two important questions:
Nanonext therefore needs its own small HTTP/1.1 body decoder, but it does not
need private nni_* functions. It should use private source only to understand
observable behavior and to design regression tests.
NNG’s current main branch identifies itself as development software with
breaking changes and directs production users to its stable branch
(NNG development notice).
The public HTTP concepts still match the proposed R API. NNG 2.0 exposes a
unified HTTP connection, asynchronous raw reads, request writes, and a response
read that deliberately stops before entity data
(NNG 2.0 HTTP operations).
The C names and ownership model differ between NNG 1.12 and 2.0. Nanonext can keep that difference inside a small set of its own HTTP functions, with one implementation for each NNG version. Its R API and body-state logic need not change.
Nanonext should not manufacture a private NNG nng_stream implementation. The
private stream layout has already changed from six operations in NNG 1.12
(1.12 private layout)
to a larger layout in NNG 2.0
(2.0 private layout).
That is exactly the kind of private coupling that a major NNG update would
break.
NNG 2.0 is the library version, not the HTTP protocol version. Its HTTP header still says that HTTP/2 is not supported (protocol note).
Rho’s HTTP interface already requires every client to implement
rho_http_send() and rho_http_open_stream()
(interface).
The current nanonext method calls only the fork’s opener
(opening adapter)
and its body stream calls only the fork’s receive function
(receive adapter).
The upstream migration is narrow:
ncurl_stream_aio() ncurl_stream()
ncurl_stream_recv() recv_aio(mode = "raw")
result$data + result$complete bytes + zero length means end
Provider translators, SSE decoding, the agent loop, cancellation, and extension interfaces do not change.
The limitation is opening. rho_task_from_function() records a closure for
later execution but does not assign it to a worker
(task construction).
When a synchronous ncurl_stream() eventually runs, it still occupies the main
R process until the response head arrives. A later ncurl_stream_aio() could
resolve to the same ordinary stream and remove that limitation without adding a
second body-receive API.
Until then, rho.http.httr2 remains the implementation that owns the connection
in a worker and relays typed heads, chunks, and completion to the caller. The
generic Rho interface makes that a client selection rather than provider logic.
The upstream implementation should cover:
chunked only as the final transfer coding, with unsupported preceding
codings rejected;HEAD, 204, 304, and interim 1xx responses;stop_aio(), timeout, and close while a receive is pending;tls_config();Two details in the fork deserve explicit tests during the rewrite. It currently
accepts chunked anywhere in Transfer-Encoding
(body selection),
and it treats every 1xx head as a bodyless completed response. Transfer coding
order and interim response handling should be settled deliberately rather than
inherited from the prototype.
ncurl_stream() while the existing
ncurl_session() remains unchanged?nanoStream or an HTTP subtype
recognized by the ordinary recv_aio(), stop_aio(), and close() paths?response = NULL, TRUE, or a character
vector like the other ncurl functions?raw(0)?close() return the same error value as an ordinary closed
nanonext stream?recv_aio() as the only receive operation?Thanks. I read “like
ncurl_session()” as a new synchronous constructor, perhapsncurl_stream(), rather than a change to the existing reusablencurlSession/transact()contract. The result would be a stream carrying the response status and requested headers;recv_aio(stream, mode = "raw")would return entity-body bytes, andraw(0)would mean end-of-stream.That shape is sufficient for Rho’s incremental SSE semantics once the response is open. It removes the fork’s separate
ncurl_stream_recv()operation and{data, complete}result. The trade-off is that connect, request write, and response-head receipt are synchronous, so Rho cannot cancel or overlap that opening phase on the main R process. I think that is reasonable for an initial implementation if the timeout is explicit. A later asynchronous constructor could resolve to the same stream without changing the body API.I checked NNG 1.12 and the current 2.0 development API. Both publicly expose the required sequence: connect, write the request, read the response head, then read raw bytes. The C names and ownership differ, but a small set of nanonext-owned HTTP functions can have one implementation for each NNG version while keeping the R API stable. I would inspect NNG internals to confirm buffering and build tests, but avoid private
nni_*symbols or a fabricated privatenng_stream; that stream layout has changed in NNG 2.0.Raw HTTP reads do not provide an incremental decoded entity body, so nanonext still needs its small fixed-length/chunked/connection-ended state machine. The existing NNG chunk helper is private and assembles the complete body.
The details I would like to confirm are whether the new constructor should preserve the ncurl
responseargument, whether cancellation or timeout makes the stream terminal, and whether repeated receives after EOF should continue returningraw(0).