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
51 changes: 47 additions & 4 deletions src/core/http_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ extern void http_response_set_default_json_flags(zend_object *, uint32_t);
# define MSG_NOSIGNAL 0
#endif

/* Half-close the send direction: SD_SEND on Winsock, SHUT_WR on POSIX. */
#ifdef _WIN32
# define HTTP_SHUT_WR SD_SEND
#else
# define HTTP_SHUT_WR SHUT_WR
#endif

/* Lingering-close budget (ms). After an error response is sent mid-upload
* we keep draining the peer's body for at most this long (refreshed on
* activity) before a forced close, so a peer that never sends FIN can't
* pin the connection open. */
#define HTTP_LINGER_CLOSE_MS 5000

#define DEFAULT_READ_BUFFER_SIZE 8192

extern zval* http_request_create_from_parsed(http_request_t *req);
Expand Down Expand Up @@ -895,6 +908,16 @@ static bool http_connection_handle_read_completion(http_connection_t *conn,
{
*should_destroy_out = false;

/* Lingering close: a 4xx was already sent + FIN'd; we are only draining
* the peer's leftover upload now. Discard every chunk (don't feed the
* parser), refresh the linger deadline, and stay armed. Peer FIN (EOF)
* is handled in the read callback (→ destroy). */
if (UNEXPECTED(conn->lingering)) {
conn->read_buffer_len = 0;
conn->deadline_ms = ZEND_ASYNC_NOW() + HTTP_LINGER_CLOSE_MS;
return true;
}

if (UNEXPECTED(!conn->protocol_detected) && !detect_and_assign_protocol(conn)) {
return true; /* Need more data for detection — caller re-arms */
}
Expand Down Expand Up @@ -968,8 +991,18 @@ static bool http_connection_handle_read_completion(http_connection_t *conn,
return false;
}

if (conn->parser != NULL) {
(void)http_connection_emit_parse_error(conn, conn->parser);
if (conn->parser != NULL && http_connection_emit_parse_error(conn, conn->parser)
&& conn->io != NULL) {
/* 4xx sent + FIN'd (shutdown in emit). Enter lingering close:
* keep reading and discarding the peer's remaining upload so the
* final close is a clean FIN, not an RST that would wipe the
* response on Windows. Peer FIN (EOF) → destroy in the read cb;
* the deadline_tick force-closes a peer that never sends FIN. */
conn->lingering = 1;
conn->read_buffer_len = 0;
conn->deadline_ms = ZEND_ASYNC_NOW() + HTTP_LINGER_CLOSE_MS;
*should_destroy_out = false;
return false;
}
*should_destroy_out = true;
return false;
Expand Down Expand Up @@ -1085,7 +1118,7 @@ static void http_connection_read_callback_fn(
* tear the conn down once it (and any pipelined chain) has finished
* responding. Without this, an EOF from a peer that sent its last
* request and shut down the write half kills mid-flight responses. */
if (conn->request_in_flight || conn->read_buffer_len > 0) {
if (!conn->lingering && (conn->request_in_flight || conn->read_buffer_len > 0)) {
conn->keep_alive = false;
return;
}
Expand All @@ -1102,7 +1135,7 @@ static void http_connection_read_callback_fn(
* could on_message_complete and dispatch a *second* handler on the same
* conn while the first one's response slot is still live. Just buffer the
* tail; handler dispose will pull it out via handle_read_completion. */
if (!terminal && conn->request_in_flight) {
if (!terminal && conn->request_in_flight && !conn->lingering) {
return;
}

Expand Down Expand Up @@ -1913,6 +1946,16 @@ bool http_connection_emit_parse_error(http_connection_t *conn, http1_parser_t *p
}

const ssize_t sent = send(fd, response, (size_t)n, MSG_NOSIGNAL);

/* Half-close our send side: flushes the response + FIN while keeping
* the recv side open so handle_read_completion can drain the peer's
* in-flight upload (lingering close) before the final closesocket.
* Without the drain, closing with unread recv data forces an RST that
* discards the just-sent response on Windows. Best-effort. */
if (sent == (ssize_t)n) {
(void)shutdown(fd, HTTP_SHUT_WR);
}

return sent == (ssize_t)n;
}
/* }}} */
Expand Down
7 changes: 7 additions & 0 deletions src/core/http_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ struct _http_connection_t {
* iterating. The destroy defers on this flag instead — see the
* gate in http_connection_destroy and the drain in http1_feed. */
unsigned in_parser_feed : 1;
/* Lingering close (graceful). Set after an error response (e.g. 413)
* is sent while the peer is still uploading: we keep reading and
* DISCARDING the peer's remaining body so the eventual close emits a
* clean FIN instead of an RST. Closing a socket with unread recv data
* forces an abortive RST on Windows, which wipes the just-sent
* response. Bounded by deadline_ms / the periodic deadline_tick. */
unsigned lingering : 1;

/* Intrusive doubly-linked node. Dual role:
* - while the slot is ALIVE, (next_conn, prev_conn) link into
Expand Down
27 changes: 18 additions & 9 deletions src/send_file.c
Original file line number Diff line number Diff line change
Expand Up @@ -479,18 +479,27 @@ static void engine_handle_stat(engine_state_t *state)
}

/* === Small-file fast path (slurp + inline body) ================== */
/* uv_fs_sendfile on Linux falls through copy_file_range (EINVAL on
* socket) into a userspace pread+write loop inside a worker thread
* — no kernel zero-copy + a futex round-trip per request. For small
* files the round-trip dominates. Slurp inline and let the protocol
* op writev(headers+body) through the same per-socket queue that
* headers normally use; ordering is then libuv's problem. */
if (!state->is_range && (size_t)state->st.st_size <= SEND_FILE_SLURP_THRESHOLD &&
file_io != NULL) {
/* uv_fs_sendfile slurps to user space then writes; on Linux it falls
* through copy_file_range (EINVAL on socket) into a pread+write loop in a
* worker thread (no zero-copy, a futex round-trip per request). On
* Windows it CANNOT target a socket at all — a Winsock SOCKET is not a
* CRT fd, so the sendfile path below delivers an EMPTY body there. For
* files within the slurp threshold — including byte-range requests, whose
* slice we cut from the in-memory buffer — read the bytes and let the
* protocol op writev(headers+body) through the normal per-socket queue
* instead. (A range/file larger than the threshold still uses sendfile
* and stays broken on Windows — separate follow-up.) */
if ((size_t)state->st.st_size <= SEND_FILE_SLURP_THRESHOLD && file_io != NULL) {
zend_string *body = fs_slurp_fd((int)file_io->descriptor.fd, (size_t)state->st.st_size);

if (body != NULL) {
http_response_static_set_body_str(response_obj, body);
if (state->is_range) {
/* Slice [range_first, range_first + body_len) from the buffer. */
http_response_static_set_body_cstr(response_obj,
ZSTR_VAL(body) + state->range_first, (size_t)body_len);
} else {
http_response_static_set_body_str(response_obj, body);
}
zend_string_release(body);

if (cfg->counters != NULL) { http_server_count_request(cfg->counters); }
Expand Down
Loading