Skip to content

TcpConnection: expose ShutdownWrite() so protocol layers can honour Connection: close #211

Description

@MDA2AV

TcpConnection has no way to close, or half-close, the connection it represents.

That is fine for the transport itself — it owns the descriptor and frees it when the refcount drops in DecRef. But a protocol layer built on ioxide routinely needs to end a connection before that, and has no API to say so. HTTP is the obvious case: Connection: close means send the response, then FIN.

Both HTTP layers on ioxide solve it the same way

Two independent consumers reach around the abstraction for the descriptor and call shutdown(2) through P/Invoke:

  • GenHTTP.Engine.Ioxide — its assembly references libc and shutdown.
  • HttpArena's ioxide entry — it did not, which is why it never closed a connection at all: the response went out carrying Connection: close and the socket stayed open, so any client that reads to EOF hung until its own timeout and the process leaked a descriptor per request. Fixed by doing the same P/Invoke.

When two separate consumers independently invent the same workaround, it is usually a missing API rather than two mistakes.

What the workaround costs

Every consumer has to:

  • reach for ClientFd, a descriptor it does not own, and hope the lifetime lines up
  • hardcode SHUT_WR = 1 and get the platform constant right
  • pick shutdown(2) over close(2) and understand why — closing frees a number the reactor still holds, so the next accept could hand the same integer to someone else. That reasoning is not obvious, and getting it wrong produces a bug that only shows up under load.

Proposal

public void ShutdownWrite();   // FIN, descriptor lifetime unchanged

Half-close is the operation that is actually wanted: the protocol layer is done sending, the transport still owns the descriptor and still closes it on DecRef. A full Close() would need to define what happens to the refcount, which ShutdownWrite() avoids entirely.

Checked on 0.4.169 and 0.7.210: conn.Close() does not compile on either, and conn.Dispose() compiles but does not close the socket.

Reproducing

Any HTTP layer on ioxide that honours Connection: close:

$ curl -sv http://localhost:8080/ -H 'Connection: close'
< HTTP/1.1 200 OK
< Connection: close
  ... socket still open 20s later, curl waits for its own timeout

Context: MDA2AV/HttpArena#1338.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions