File Coverage

feersum_h1.c.inc
Criterion Covered Total %
statement 514 616 83.4
branch 299 422 70.8
condition n/a
subroutine n/a
pod n/a
total 813 1038 78.3


line stmt bran cond sub pod time code
1             static int
2 662           try_parse_http(struct feer_conn *c, size_t last_read)
3             {
4 662           struct feer_req *req = c->req;
5 662 100         if (likely(!req)) {
6 646 100         FEER_REQ_ALLOC(req);
7 646           c->req = req;
8             }
9              
10             // phr_parse_request resets num_headers on each call; must restore to max
11 662           req->num_headers = MAX_HEADERS;
12              
13 1324           return phr_parse_request(SvPVX(c->rbuf), SvCUR(c->rbuf),
14             &req->method, &req->method_len,
15             &req->uri, &req->uri_len, &req->minor_version,
16 662           req->headers, &req->num_headers,
17 662           (SvCUR(c->rbuf)-last_read));
18             }
19              
20             // Finish receiving: transition to RECEIVE_SHUTDOWN and stop read I/O
21             INLINE_UNLESS_DEBUG static void
22 737           finish_receiving(struct feer_conn *c) {
23 737           change_receiving_state(c, RECEIVE_SHUTDOWN);
24 737           stop_read_watcher(c);
25 737           stop_read_timer(c);
26 737           stop_header_timer(c);
27 737           }
28              
29             static void
30 751           try_conn_read(EV_P_ ev_io *w, int revents)
31             {
32 751           dCONN;
33 751           SvREFCNT_inc_void_NN(c->self);
34 751           feer_conn_set_busy(c);
35 751           ssize_t got_n = 0;
36              
37 751 100         if (unlikely(c->pipelined)) goto pipelined;
38              
39             // if it's marked readable EV suggests we simply try read it. Otherwise it
40             // is stopped and we should ditch this connection.
41 646 50         if (unlikely(revents & EV_ERROR && !(revents & EV_READ))) {
    0          
42             trace("EV error on read, fd=%d revents=0x%08x\n", w->fd, revents);
43 0           goto try_read_error;
44             }
45              
46 646 50         if (unlikely(c->receiving == RECEIVE_SHUTDOWN))
47 0           goto dont_read_again;
48              
49             trace("try read %d\n",w->fd);
50              
51 646 100         if (unlikely(!c->rbuf)) { // unlikely = optimize for keepalive requests
52             trace("init rbuf for %d\n",w->fd);
53 594           c->rbuf = newSV(READ_BUFSZ + 1);
54 594           SvPOK_on(c->rbuf);
55             }
56              
57 646           ssize_t space_free = SvLEN(c->rbuf) - SvCUR(c->rbuf);
58 646 100         if (unlikely(space_free < READ_BUFSZ)) { // unlikely = optimize for small
59 29           size_t cur_len = SvLEN(c->rbuf);
60             // DoS protection: limit buffer growth (especially for chunked encoding)
61 29 50         if (unlikely(cur_len + READ_GROW_FACTOR*READ_BUFSZ > c->cached_max_read_buf)) {
62             trace("buffer too large %d: %"Sz_uf" > %"Sz_uf"\n",
63             w->fd, (Sz)cur_len, (Sz)c->cached_max_read_buf);
64 0           respond_with_server_error(c, "Request too large\n", 413);
65 0           goto try_read_error;
66             }
67 29           size_t new_len = cur_len + READ_GROW_FACTOR*READ_BUFSZ;
68             trace("moar memory %d: %"Sz_uf" to %"Sz_uf"\n",
69             w->fd, (Sz)SvLEN(c->rbuf), (Sz)new_len);
70 29 50         SvGROW(c->rbuf, new_len);
    50          
71 29           space_free += READ_GROW_FACTOR*READ_BUFSZ;
72             }
73              
74 646           char *cur = SvPVX(c->rbuf) + SvCUR(c->rbuf);
75 646           got_n = read(w->fd, cur, space_free);
76              
77 646 100         if (unlikely(got_n <= 0)) {
78 31 100         if (unlikely(got_n == 0)) {
79             trace("EOF before complete request: fd=%d buf=%"Sz_uf"\n", w->fd, (Sz)SvCUR(c->rbuf));
80 29           goto try_read_error;
81             }
82 2 50         if (likely(errno == EAGAIN || errno == EINTR))
    50          
83 0           goto try_read_again;
84 2           trouble("try_conn_read fd=%d: %s\n", w->fd, strerror(errno));
85 2           goto try_read_error;
86             }
87              
88             trace("read %d %"Ssz_df"\n", w->fd, (Ssz)got_n);
89 615           SvCUR(c->rbuf) += got_n;
90 615 100         if (c->receiving == RECEIVE_WAIT)
91 12           change_receiving_state(c, RECEIVE_HEADERS);
92 615           goto try_parse;
93              
94 105           pipelined:
95 105           got_n = c->pipelined;
96 105           c->pipelined = 0;
97              
98 720           try_parse:
99             // Handle PROXY protocol header state
100 753 100         if (unlikely(c->receiving == RECEIVE_PROXY_HEADER)) {
101 136           int ret = try_parse_proxy_header(c);
102 136 100         if (ret == -1) {
103 101           respond_with_server_error(c, "Invalid PROXY protocol header\n", 400);
104 101           goto dont_read_again;
105             }
106 35 100         if (ret == -2) goto try_read_again_reset_timer;
107              
108             // Consume parsed bytes from buffer
109 33           STRLEN remaining = SvCUR(c->rbuf) - ret;
110 33 50         if (remaining > 0)
111 33           memmove(SvPVX(c->rbuf), SvPVX(c->rbuf) + ret, remaining);
112 33           SvCUR_set(c->rbuf, remaining);
113              
114             // Clear cached remote addr/port (force regeneration with new address)
115 33 50         feer_clear_remote_cache(c);
    50          
116              
117             // Transition to HTTP parsing
118 33           change_receiving_state(c, RECEIVE_HEADERS);
119 33 50         if (remaining > 0) {
120 33           got_n = remaining;
121 33           goto try_parse;
122             }
123 0           goto try_read_again_reset_timer;
124             }
125              
126             // likely = optimize for small requests
127 617 100         if (likely(c->receiving <= RECEIVE_HEADERS)) {
128 603           int ret = try_parse_http(c, (size_t)got_n);
129 603 100         if (ret == -1) goto try_read_bad;
130             #ifdef TCP_DEFER_ACCEPT
131 590 100         if (ret == -2) goto try_read_again_reset_timer;
132             #else
133             if (ret == -2) {
134             if (c->cached_is_tcp) goto try_read_again;
135             else goto try_read_again_reset_timer;
136             }
137             #endif
138              
139 568 100         if (process_request_headers(c, ret))
140 17           goto try_read_again_reset_timer;
141             else
142 551           goto dont_read_again;
143             }
144 14 100         else if (likely(c->receiving == RECEIVE_BODY)) {
145 10           c->received_cl += got_n;
146 10 100         if (c->received_cl < c->expected_cl)
147 1           goto try_read_again_reset_timer;
148             // body is complete
149 9           sched_request_callback(c);
150 9           goto dont_read_again;
151             }
152 4 50         else if (c->receiving == RECEIVE_CHUNKED) {
153             // Try to parse chunked data
154 4           int ret = try_parse_chunked(c);
155 4 50         if (ret == 1)
156 0           goto try_read_again_reset_timer;
157 4 50         if (ret == -1) {
158 0           respond_with_server_error(c, "Malformed chunked encoding\n", 400);
159 0           goto dont_read_again;
160             }
161             // chunked body is complete
162 4           sched_request_callback(c);
163 4           goto dont_read_again;
164             }
165 0 0         else if (c->receiving == RECEIVE_STREAMING) {
166             // Streaming body read with poll_read_cb
167 0           c->received_cl += got_n;
168 0 0         if (c->poll_read_cb) {
169 0           call_poll_callback(c, 0); // 0 = read callback
170             }
171             // Callback may have closed/errored — check before re-arming
172 0 0         if (c->receiving >= RECEIVE_SHUTDOWN)
173 0           goto dont_read_again;
174             // Check if body is complete (if Content-Length was specified)
175 0 0         if (c->expected_cl > 0 && c->received_cl >= c->expected_cl) {
    0          
176 0           goto dont_read_again;
177             }
178 0           goto try_read_again_reset_timer;
179             }
180             else {
181 0           trouble("unknown read state %d %d", w->fd, c->receiving);
182             }
183              
184             // fallthrough:
185 31           try_read_error:
186             trace("READ ERROR %d, refcnt=%d\n", w->fd, SvREFCNT(c->self));
187 31           change_receiving_state(c, RECEIVE_SHUTDOWN);
188 31           change_responding_state(c, RESPOND_SHUTDOWN);
189 31           stop_all_watchers(c);
190 31           goto try_read_cleanup;
191              
192 13           try_read_bad:
193             trace("bad request %d\n", w->fd);
194 13           respond_with_server_error(c, "Malformed request\n", 400);
195             // fallthrough (respond_with_server_error sets is_keepalive=0):
196 678           dont_read_again:
197             trace("done reading %d\n", w->fd);
198 678           finish_receiving(c);
199 678           goto try_read_cleanup;
200              
201 42           try_read_again_reset_timer:
202             trace("(reset read timer) %d\n", w->fd);
203 42           restart_read_timer(c);
204             // fallthrough:
205 42           try_read_again:
206             trace("read again %d\n", w->fd);
207 42           start_read_watcher(c);
208              
209 751           try_read_cleanup:
210 751           SvREFCNT_dec(c->self);
211 751           }
212              
213             static void
214 6           conn_read_timeout (EV_P_ ev_timer *w, int revents)
215             {
216 6           dCONN;
217 6           SvREFCNT_inc_void_NN(c->self);
218              
219 6 50         if (unlikely(!(revents & EV_TIMER) || c->receiving == RECEIVE_SHUTDOWN)) {
    50          
220             // if there's no EV_TIMER then EV has stopped it on an error
221 0 0         if (revents & EV_ERROR)
222 0           trouble("EV error on read timer, fd=%d revents=0x%08x\n",
223             c->fd,revents);
224 0           goto read_timeout_cleanup;
225             }
226              
227             trace("read timeout %d\n", c->fd);
228              
229             #ifdef FEERSUM_HAS_H2
230             if (c->h2_session) {
231             /* H2 parent connection idle timeout: send GOAWAY and close.
232             * Cannot use respond_with_server_error() which writes HTTP/1.1. */
233             trace("H2 idle timeout fd=%d\n", c->fd);
234             if (!c->h2_goaway_sent) {
235             nghttp2_submit_goaway(c->h2_session, NGHTTP2_FLAG_NONE,
236             nghttp2_session_get_last_proc_stream_id(c->h2_session),
237             NGHTTP2_NO_ERROR, NULL, 0);
238             c->h2_goaway_sent = 1;
239             feer_h2_session_send(c);
240             }
241             feer_shutdown_conn(c, "H2 idle timeout");
242             goto read_timeout_cleanup;
243             }
244             #endif
245              
246 11 50         if (likely(c->responding == RESPOND_NOT_STARTED) && c->receiving >= RECEIVE_HEADERS) {
    100          
247             #ifdef FEERSUM_HAS_TLS
248 5 50         if (c->tls && !c->tls_handshake_done) {
    0          
249 0           feer_shutdown_conn(c, "TLS handshake timeout (read)");
250 0           goto read_timeout_cleanup;
251             }
252             #endif
253             const char *msg;
254 5 50         if (c->receiving == RECEIVE_PROXY_HEADER) {
255 0           msg = "PROXY protocol header timeout.";
256             }
257 5 100         else if (c->receiving == RECEIVE_HEADERS) {
258 2           msg = "Headers took too long.";
259             }
260             else {
261 3           msg = "Timeout reading body.";
262             }
263 5           respond_with_server_error(c, msg, 408);
264             } else {
265             trace("read timeout in keepalive conn: %d\n", c->fd);
266 1           feer_shutdown_conn(c, "close at read timeout");
267 1           change_receiving_state(c, RECEIVE_SHUTDOWN);
268             }
269              
270 6           read_timeout_cleanup:
271 6           SvREFCNT_dec(c->self);
272 6           }
273              
274             // Slowloris protection: non-resetting deadline for header completion
275             static void
276 8           conn_header_timeout (EV_P_ ev_timer *w, int revents)
277             {
278 8           dCONN;
279 8           SvREFCNT_inc_void_NN(c->self);
280              
281 8 50         if (unlikely(!(revents & EV_TIMER) || c->receiving == RECEIVE_SHUTDOWN)) {
    50          
282 0 0         if (revents & EV_ERROR)
283 0           trouble("EV error on header timer, fd=%d revents=0x%08x\n",
284             c->fd, revents);
285 0           goto header_timeout_cleanup;
286             }
287              
288             // Only trigger if still receiving headers (including PROXY protocol phase)
289 8 50         if ((c->receiving == RECEIVE_HEADERS || c->receiving == RECEIVE_PROXY_HEADER)
    0          
290 8 50         && c->responding == RESPOND_NOT_STARTED) {
291             trace("header deadline timeout %d (Slowloris protection)\n", c->fd);
292             #ifdef FEERSUM_HAS_TLS
293 8 100         if (c->tls && !c->tls_handshake_done) {
    50          
294 4           feer_shutdown_conn(c, "TLS handshake timeout");
295             } else
296             #endif
297             {
298 4           respond_with_server_error(c, "Header timeout (possible Slowloris attack)\n", 408);
299             }
300             }
301              
302 0           header_timeout_cleanup:
303             // One-shot timer: libev already stopped it before invoking this callback.
304 8           SvREFCNT_dec(c->self); // balances timer start (setup_accepted_conn or keepalive)
305 8           SvREFCNT_dec(c->self); // balances callback protection at top of this function
306 8           }
307              
308             static void
309 0           conn_write_timeout (EV_P_ ev_timer *w, int revents)
310             {
311 0           dCONN;
312 0           SvREFCNT_inc_void_NN(c->self);
313              
314 0 0         if (unlikely(!(revents & EV_TIMER) || c->responding == RESPOND_SHUTDOWN)) {
    0          
315 0 0         if (revents & EV_ERROR)
316 0           trouble("EV error on write timer, fd=%d revents=0x%08x\n",
317             c->fd, revents);
318 0           goto write_timeout_cleanup;
319             }
320              
321             trace("write timeout %d\n", c->fd);
322 0           feer_shutdown_conn(c, "write timeout");
323 0           change_receiving_state(c, RECEIVE_SHUTDOWN);
324              
325 0           write_timeout_cleanup:
326 0           SvREFCNT_dec(c->self);
327 0           }
328              
329             // Helper to set up a newly accepted connection
330             // Returns 0 on success, -1 on error (fd already closed on error)
331             static int
332 656           setup_accepted_conn(EV_P_ int fd, struct sockaddr *sa, socklen_t sa_len,
333             struct feer_server *srvr, struct feer_listen *lsnr)
334             {
335 656 50         if (unlikely(prep_socket(fd, lsnr->is_tcp))) {
336 0           trouble("prep_socket failed for fd=%d: %s\n", fd, strerror(errno));
337 0 0         if (unlikely(close(fd) < 0))
338 0           trouble("close(prep_socket error) fd=%d: %s\n", fd, strerror(errno));
339 0           return -1;
340             }
341              
342 656           struct feer_conn *c = new_feer_conn(EV_A, fd, sa, sa_len, srvr, lsnr);
343              
344             // Slowloris protection: start non-resetting header deadline timer
345 656 100         if (srvr->header_timeout > 0.0) {
346 655           ev_timer_set(&c->header_ev_timer, srvr->header_timeout, 0.0); // one-shot
347 655           ev_timer_start(feersum_ev_loop, &c->header_ev_timer);
348 655           SvREFCNT_inc_void_NN(c->self);
349             trace("started header deadline timer %d (%.1fs)\n", c->fd, srvr->header_timeout);
350             }
351              
352             #ifdef TCP_DEFER_ACCEPT
353             // With TCP_DEFER_ACCEPT, data is already available
354             #ifdef FEERSUM_HAS_TLS
355 656 100         if (lsnr->tls_ctx_ref) {
356             // TLS: don't try immediate read with deferred accept, just start watcher.
357             // The TLS handshake needs proper event-driven I/O.
358 68           start_read_watcher(c);
359             } else
360             #endif
361             {
362 588           try_conn_read(EV_A, &c->read_ev_io, EV_READ);
363             }
364             assert(SvREFCNT(c->self) <= (srvr->header_timeout > 0.0 ? 4 : 3));
365             #else
366             if (lsnr->is_tcp) {
367             start_read_watcher(c);
368             restart_read_timer(c);
369             assert(SvREFCNT(c->self) == (srvr->header_timeout > 0.0 ? 4 : 3));
370             } else {
371             #ifdef FEERSUM_HAS_TLS
372             if (lsnr->tls_ctx_ref)
373             try_tls_conn_read(EV_A, &c->read_ev_io, EV_READ);
374             else
375             #endif
376             try_conn_read(EV_A, &c->read_ev_io, EV_READ);
377             assert(SvREFCNT(c->self) <= (srvr->header_timeout > 0.0 ? 4 : 3));
378             }
379             #endif
380 656           SvREFCNT_dec(c->self);
381 656           return 0;
382             }
383              
384             static void
385 0           emfile_retry_cb (EV_P_ ev_timer *w, int revents)
386             {
387 0           struct feer_listen *lsnr = (struct feer_listen *)w->data;
388 0 0         if (lsnr->server->shutting_down) return;
389             trace("EMFILE retry: clearing emfile pause on fd=%d\n", lsnr->fd);
390 0           lsnr->pause_flags &= ~FEER_PAUSE_EMFILE;
391             /* Only restart if no other reason still wants us paused */
392 0 0         if (!lsnr->pause_flags && lsnr->fd >= 0)
    0          
393 0           ev_io_start(EV_A, &lsnr->accept_w);
394             }
395              
396             /*
397             * Accept a single connection from the listen socket.
398             * Returns: 1 = continue accepting, 0 = stop (EAGAIN/error/limit)
399             */
400             static int
401 1237           try_accept_one(EV_P_ struct feer_listen *lsnr, struct feer_server *srvr)
402             {
403             struct sockaddr_storage sa_buf;
404 1237           socklen_t sa_len = sizeof(struct sockaddr_storage);
405 1237           errno = 0;
406             #ifdef HAS_ACCEPT4
407 1237           int fd = accept4(lsnr->fd, (struct sockaddr *)&sa_buf, &sa_len, SOCK_CLOEXEC|SOCK_NONBLOCK);
408             #else
409             int fd = accept(lsnr->fd, (struct sockaddr *)&sa_buf, &sa_len);
410             #endif
411             trace("accepted fd=%d, errno=%d\n", fd, errno);
412 1237 100         if (fd == -1) {
413 580 50         if (errno == EINTR) return 1;
414 580 50         if (errno == EMFILE || errno == ENFILE) {
    50          
415 0           trouble("accept: %s — pausing accept for 1s on fd=%d\n",
416             strerror(errno), lsnr->fd);
417 0           ev_io_stop(EV_A, &lsnr->accept_w);
418 0           lsnr->pause_flags |= FEER_PAUSE_EMFILE;
419 0           ev_timer_set(&lsnr->emfile_w, 1.0, 0.0);
420 0           ev_timer_start(EV_A, &lsnr->emfile_w);
421             }
422 580           return 0;
423             }
424              
425 657 100         if (srvr->max_connections > 0 && srvr->active_conns >= srvr->max_connections) {
    100          
426 3 100         if (!feer_server_recycle_idle_conn(srvr)) {
427             /* At capacity with nothing to recycle: pause the listener so we
428             * don't busy-spin accepting-and-closing. DESTROY will clear the
429             * cap pause as soon as a connection is freed. */
430             trace("max_connections limit reached (%d), pausing accept on fd=%d\n",
431             srvr->max_connections, lsnr->fd);
432 1           close(fd);
433 1           ev_io_stop(EV_A, &lsnr->accept_w);
434 1           lsnr->pause_flags |= FEER_PAUSE_CAP;
435 1           return 0;
436             }
437             }
438              
439 656           setup_accepted_conn(EV_A, fd, (struct sockaddr *)&sa_buf, sa_len, srvr, lsnr);
440 656           return 1;
441             }
442              
443             #ifdef __linux__
444             static void
445 0           accept_epoll_cb (EV_P_ ev_io *w, int revents)
446             {
447 0           struct feer_listen *lsnr = (struct feer_listen *)w->data;
448 0           struct feer_server *srvr = lsnr->server;
449             struct epoll_event events[1];
450              
451 0 0         if (unlikely(srvr->shutting_down)) {
452 0           ev_io_stop(EV_A, w);
453 0           return;
454             }
455              
456 0 0         if (unlikely(revents & EV_ERROR)) {
457 0           trouble("EV error in accept_epoll_cb, fd=%d, revents=0x%08x\n", w->fd, revents);
458 0           ev_break(EV_A, EVBREAK_ALL);
459 0           return;
460             }
461              
462 0           int accept_count = 0;
463 0 0         while (accept_count++ < srvr->max_accept_per_loop) {
464 0 0         if (epoll_wait(lsnr->epoll_fd, events, 1, 0) <= 0) break;
465 0 0         if (!try_accept_one(EV_A, lsnr, srvr)) break;
466             }
467             }
468             #endif
469              
470             static void
471 581           accept_cb (EV_P_ ev_io *w, int revents)
472             {
473 581           struct feer_listen *lsnr = (struct feer_listen *)w->data;
474 581           struct feer_server *srvr = lsnr->server;
475              
476 581 50         if (unlikely(srvr->shutting_down)) {
477 0           ev_io_stop(EV_A, w);
478 0           return;
479             }
480              
481 581 50         if (unlikely(revents & EV_ERROR)) {
482 0           trouble("EV error in accept_cb, fd=%d, revents=0x%08x\n",w->fd,revents);
483 0           ev_break(EV_A, EVBREAK_ALL);
484 0           return;
485             }
486              
487             trace2("accept! revents=0x%08x\n", revents);
488              
489 581           int accept_count = 0;
490 1237 50         while (accept_count++ < srvr->max_accept_per_loop) {
491 1237 100         if (!try_accept_one(EV_A, lsnr, srvr)) break;
492             }
493             }
494              
495             // Helper to set up the accept watcher, with optional EPOLLEXCLUSIVE on Linux
496             static void
497 220           setup_accept_watcher(struct feer_listen *lsnr, int listen_fd)
498             {
499 220           struct feer_server *srvr = lsnr->server;
500             #if defined(__linux__) && defined(EPOLLEXCLUSIVE)
501 220 100         if (srvr->use_epoll_exclusive) {
502             // Create a separate epoll fd for the accept socket with EPOLLEXCLUSIVE
503             // This avoids thundering herd in prefork without patching libev
504             struct epoll_event ev;
505 2           lsnr->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
506 2 50         if (lsnr->epoll_fd < 0) {
507 0           trouble("epoll_create1 for accept: %s\n", strerror(errno));
508 0           croak("Failed to create accept epoll fd");
509             }
510              
511 2           ev.events = EPOLLIN | EPOLLEXCLUSIVE;
512 2           ev.data.fd = listen_fd;
513 2 50         if (epoll_ctl(lsnr->epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
514 0           trouble("epoll_ctl EPOLL_CTL_ADD for accept fd=%d: %s\n", listen_fd, strerror(errno));
515 0 0         if (unlikely(close(lsnr->epoll_fd) < 0))
516 0           trouble("close(lsnr->epoll_fd) fd=%d: %s\n", lsnr->epoll_fd, strerror(errno));
517 0           lsnr->epoll_fd = -1;
518 0           croak("Failed to add listen socket to accept epoll");
519             }
520              
521             trace("created lsnr->epoll_fd=%d with EPOLLEXCLUSIVE for listen fd=%d\n",
522             lsnr->epoll_fd, listen_fd);
523              
524             // Watch the lsnr->epoll_fd instead of the listen socket directly
525             // When lsnr->epoll_fd becomes readable, only THIS worker was selected
526 2           ev_io_init(&lsnr->accept_w, accept_epoll_cb, lsnr->epoll_fd, EV_READ);
527             } else {
528             // Standard mode: watch listen socket directly
529 218           ev_io_init(&lsnr->accept_w, accept_cb, listen_fd, EV_READ);
530             }
531             #else
532             // Non-Linux or no EPOLLEXCLUSIVE: standard mode only
533             ev_io_init(&lsnr->accept_w, accept_cb, listen_fd, EV_READ);
534             #endif
535 220           lsnr->accept_w.data = (void *)lsnr;
536 220           ev_set_priority(&lsnr->accept_w, srvr->accept_priority);
537 220           ev_init(&lsnr->emfile_w, emfile_retry_cb);
538 220           lsnr->emfile_w.data = (void *)lsnr;
539 220           }
540              
541             static void
542 548           sched_request_callback (struct feer_conn *c)
543             {
544 548           struct feer_server *server = c->server;
545             if (FEERSUM_REQ_BODY_ENABLED()) {
546             FEERSUM_REQ_BODY(c->fd, (size_t)(c->received_cl >= 0 ? c->received_cl : 0));
547             }
548             trace("sched req callback: %d c=%p, head=%p\n", c->fd, c, server->request_ready_rinq);
549 548           rinq_push(&server->request_ready_rinq, c);
550 548           SvREFCNT_inc_void_NN(c->self); // for the rinq
551 548 100         if (!ev_is_active(&server->ei)) {
552             trace("starting idle watcher\n");
553 384           ev_idle_start(feersum_ev_loop, &server->ei);
554             }
555 548           }
556              
557             // Parse chunked transfer encoding from rbuf
558             // Returns: 1 if need more data, 0 if complete, -1 if parse error
559             // Decodes chunks in-place: moves decoded data to beginning of rbuf
560             // c->received_cl tracks decoded bytes, c->chunk_remaining tracks state (see CHUNK_STATE_*)
561             static int
562 71           try_parse_chunked (struct feer_conn *c)
563             {
564 71 50         if (!c->rbuf) return 1; // need data
565              
566 71           char *buf = SvPVX(c->rbuf);
567 71           STRLEN buf_len = SvCUR(c->rbuf);
568             // received_cl tracks decoded position; should always be non-negative
569 71           STRLEN read_pos = (c->received_cl >= 0) ? (STRLEN)c->received_cl : 0;
570 71           STRLEN write_pos = read_pos; // decoded data position
571              
572             trace("try_parse_chunked fd=%d buf_len=%"Sz_uf" read_pos=%"Sz_uf" chunk_remaining=%"Ssz_df"\n",
573             c->fd, (Sz)buf_len, (Sz)read_pos, (Ssz)c->chunk_remaining);
574              
575 5525 100         while (read_pos < buf_len) {
576 5523 100         if (c->chunk_remaining == CHUNK_STATE_NEED_CRLF) {
577             // Need CRLF after chunk data
578 2           STRLEN remaining = buf_len - read_pos;
579 2 50         if (remaining < 2)
580 0           goto need_more;
581 2 50         if (buf[read_pos] != '\r' || buf[read_pos+1] != '\n') {
    50          
582             trace("chunked: missing CRLF after chunk data\n");
583 0           return -1; // parse error
584             }
585 2           read_pos += 2;
586 2           c->chunk_remaining = CHUNK_STATE_PARSE_SIZE; // ready for next chunk size
587 2           continue;
588             }
589 5521 100         else if (c->chunk_remaining == CHUNK_STATE_PARSE_SIZE) {
590             // Parsing chunk size line: find CRLF
591 2766           char *line_start = buf + read_pos;
592 2766           char *line_end = NULL;
593 2766           STRLEN remaining = buf_len - read_pos;
594             STRLEN i;
595              
596             // Look for CRLF
597 5856 50         for (i = 0; i + 1 < remaining; i++) {
598 5856 100         if (line_start[i] == '\r' && line_start[i+1] == '\n') {
    50          
599 2766           line_end = line_start + i;
600 2766           break;
601             }
602             }
603              
604 2766 50         if (!line_end) {
605             // Need more data for chunk size line
606             trace("chunked: need more data for chunk size line\n");
607 0           goto need_more;
608             }
609              
610             // Parse hex chunk size (stop at ; for extensions)
611             // Uses hex_decode_table for fast lookup (0-15 for valid, 0xFF for invalid)
612 2766           UV chunk_size = 0;
613 2766           int hex_digits = 0;
614 2766           char *p = line_start;
615 5548 100         while (p < line_end) {
616 2798           unsigned char ch = (unsigned char)*p;
617 2798           unsigned char val = hex_decode_table[ch];
618 2798 100         if (val != 0xFF) {
619 2783           hex_digits++;
620             // Check for potential overflow BEFORE shifting
621 2783 100         if (chunk_size > (UV_MAX >> 4)) {
622             trace("chunked: chunk size overflow\n");
623 1           return -1;
624             }
625 2782           chunk_size = (chunk_size << 4) | val;
626             }
627 15 100         else if (ch == ';' || ch == ' ' || ch == '\t') {
    100          
    50          
628             break; // chunk extension or whitespace, stop parsing
629             }
630             else {
631             trace("chunked: invalid hex char '%c'\n", ch);
632 6           return -1; // parse error
633             }
634 2782           p++;
635             }
636              
637             // Reject chunk size lines with no hex digits (e.g., ";ext\r\n")
638 2759 100         if (hex_digits == 0) {
639             trace("chunked: no hex digits in chunk size\n");
640 4           return -1; // parse error
641             }
642              
643             trace("chunked: parsed size=%"UVuf" at pos=%"Sz_uf"\n",
644             chunk_size, (Sz)(line_start - buf));
645              
646             // Move past the CRLF
647 2755           read_pos = (line_end - buf) + 2;
648              
649 2755 100         if (chunk_size == 0) {
650             // Final chunk - transition to trailer parsing state.
651             // The c->chunk_remaining == 0 handler below does the actual
652             // trailer scan (both for first entry and re-entry after needing
653             // more data), so just set state and let the while loop continue.
654 52           c->trailer_count = 0;
655 52           c->chunk_remaining = 0;
656 52           continue;
657             }
658              
659             // Check cumulative body size (prevent overflow on 32-bit)
660             // Split into two checks to avoid unsigned underflow when
661             // chunk_size > MAX_BODY_LEN (which would wrap the subtraction)
662 2703 50         if (unlikely(chunk_size > (UV)c->cached_max_body_len)) {
663             trace("chunked: chunk too large %"UVuf"\n", chunk_size);
664 0           return -1; // error
665             }
666 2703 50         if (unlikely(write_pos > (STRLEN)(c->cached_max_body_len - chunk_size))) {
667             trace("chunked: body too large %"UVuf" + %"Sz_uf"\n",
668             chunk_size, (Sz)write_pos);
669 0           return -1; // error
670             }
671              
672             // DoS protection: limit number of chunks
673 2703           c->chunk_count++;
674 2703 50         if (unlikely(c->chunk_count > MAX_CHUNK_COUNT)) {
675             trace("chunked: too many chunks (%u)\n", c->chunk_count);
676 0           return -1; // error
677             }
678              
679 2703 50         if (chunk_size > (UV)SSIZE_MAX) return -1;
680 2703           c->chunk_remaining = (ssize_t)chunk_size;
681             }
682 2755 100         else if (c->chunk_remaining == 0) {
683             // We've seen the 0 chunk, looking for trailer end.
684             // c->trailer_count was initialized when we first saw the 0-chunk.
685 52           STRLEN remaining = buf_len - read_pos;
686 52           char *trailer_start = buf + read_pos;
687 52           STRLEN i = 0;
688 5794 50         while (i + 1 < remaining) {
689 5794 100         if (trailer_start[i] == '\r' && trailer_start[i+1] == '\n') {
    50          
690 317 100         if (i == 0) {
691             // Empty line - done (chunk_remaining already 0)
692 50           read_pos += 2; // skip terminating \r\n
693 50 100         STRLEN pipelined = (read_pos < buf_len) ? buf_len - read_pos : 0;
694 50           c->expected_cl = write_pos;
695 50           c->received_cl = write_pos + pipelined;
696 50 100         if (pipelined > 0)
697 2           memmove(buf + write_pos, buf + read_pos, pipelined);
698 50           SvCUR_set(c->rbuf, write_pos + pipelined);
699 50           return 0; // complete
700             }
701             // Skip trailer header
702 267 100         if (unlikely(++c->trailer_count > MAX_TRAILER_HEADERS)) {
703             trace("chunked: too many trailer headers\n");
704 2           return -1; // error
705             }
706 265           read_pos += i + 2;
707 265           remaining = buf_len - read_pos;
708 265           trailer_start = buf + read_pos;
709 265           i = 0; // restart from beginning
710 265           continue;
711             }
712 5477           i++;
713             }
714 0           goto need_more;
715             }
716             else {
717             // chunk_remaining > 0: copy chunk data
718 2703           STRLEN remaining = buf_len - read_pos;
719 2703           STRLEN to_copy = (STRLEN)c->chunk_remaining;
720 2703 100         if (to_copy > remaining)
721 2           to_copy = remaining;
722              
723             // Move chunk data to write position (decode in place)
724 2703 50         if (write_pos != read_pos && to_copy > 0) {
    50          
725 2703           memmove(buf + write_pos, buf + read_pos, to_copy);
726             }
727 2703           write_pos += to_copy;
728 2703           read_pos += to_copy;
729 2703           c->chunk_remaining -= to_copy;
730 2703           c->received_cl = write_pos;
731              
732 2703 100         if (c->chunk_remaining > 0) {
733             // Need more data for this chunk
734 2           goto need_more;
735             }
736              
737             // Chunk complete, need to consume trailing CRLF
738 2701           remaining = buf_len - read_pos;
739 2701 100         if (remaining < 2) {
740             // Need CRLF
741 2           c->chunk_remaining = CHUNK_STATE_NEED_CRLF;
742 2           goto need_more;
743             }
744 2699 100         if (buf[read_pos] != '\r' || buf[read_pos+1] != '\n') {
    50          
745             trace("chunked: missing CRLF after chunk data\n");
746 2           return -1; // parse error
747             }
748 2697           read_pos += 2;
749 2697           c->chunk_remaining = CHUNK_STATE_PARSE_SIZE; // ready for next chunk size
750             }
751             }
752              
753 2           need_more:
754             // Compact buffer: move unparsed data to after decoded data
755 6 100         if (read_pos > write_pos) {
756 4 50         if (read_pos < buf_len) {
757 0           STRLEN unparsed = buf_len - read_pos;
758 0           memmove(buf + write_pos, buf + read_pos, unparsed);
759 0           SvCUR_set(c->rbuf, write_pos + unparsed);
760             } else {
761 4           SvCUR_set(c->rbuf, write_pos);
762             }
763             }
764             // else: read_pos == write_pos, buffer is already compact, SvCUR unchanged
765 6           c->received_cl = write_pos;
766 6           return 1; // need more data
767             }
768              
769             // the unlikely/likely annotations here are trying to optimize for GET first
770             // and POST second. Other entity-body requests are third in line.
771             static bool
772 627           process_request_headers (struct feer_conn *c, int body_offset)
773             {
774             int err_code;
775             const char *err;
776 627           struct feer_req *req = c->req;
777              
778             if (FEERSUM_REQ_NEW_ENABLED()) {
779             char m[16], u[1024];
780             STRLEN m_len = (req->method_len < 15) ? req->method_len : 15;
781             STRLEN u_len = (req->uri_len < 1023) ? req->uri_len : 1023;
782             memcpy(m, req->method, m_len); m[m_len] = '\0';
783             memcpy(u, req->uri, u_len); u[u_len] = '\0';
784             FEERSUM_REQ_NEW(c->fd, m, u);
785             }
786              
787             // Slowloris protection: headers complete, stop deadline timer
788 627           stop_header_timer(c);
789              
790             trace("processing headers %d minor_version=%d\n",c->fd,req->minor_version);
791 627           bool body_is_required = 0;
792 627           bool next_req_follows = 0;
793 627           bool got_content_length = 0;
794              
795 627           c->is_http11 = (req->minor_version == 1);
796 627 100         c->is_keepalive = c->cached_keepalive_default && c->is_http11;
    100          
797 627           c->expect_continue = 0;
798 627           c->receive_chunked = 0;
799 627           c->reqs++;
800              
801 627           change_receiving_state(c, RECEIVE_BODY);
802 627           c->expected_cl = 0;
803 627           c->received_cl = 0;
804              
805             // Dispatch by method length first to minimize string comparisons
806 627           switch (req->method_len) {
807 451           case 3:
808 451 100         if (likely(memcmp(req->method, "GET", 3) == 0)) {
809 450           next_req_follows = 1;
810 1 50         } else if (memcmp(req->method, "PUT", 3) == 0) {
811 1           body_is_required = 1;
812             } else {
813 0           goto unsupported_method;
814             }
815 451           break;
816 167           case 4:
817 167 100         if (likely(memcmp(req->method, "POST", 4) == 0)) {
818 165           body_is_required = 1;
819 2 50         } else if (memcmp(req->method, "HEAD", 4) == 0) {
820 2           next_req_follows = 1;
821             } else {
822 0           goto unsupported_method;
823             }
824 167           break;
825 2           case 5:
826 2 50         if (memcmp(req->method, "PATCH", 5) == 0) {
827 2           body_is_required = 1;
828             } else {
829 0           goto unsupported_method;
830             }
831 2           break;
832 3           case 6:
833 3 100         if (memcmp(req->method, "DELETE", 6) == 0) {
834 2           next_req_follows = 1;
835             } else {
836 1           goto unsupported_method;
837             }
838 2           break;
839 2           case 7:
840 2 50         if (memcmp(req->method, "OPTIONS", 7) == 0) {
841 2           next_req_follows = 1;
842             } else {
843 0           goto unsupported_method;
844             }
845 2           break;
846             default:
847 3           unsupported_method:
848 3           err = "Feersum doesn't support that method yet\n";
849 3           err_code = 405;
850 3           goto got_bad_request;
851             }
852              
853             // RFC 7230: URI length check (414 URI Too Long)
854 624 100         if (unlikely(req->uri_len > c->cached_max_uri_len)) {
855 3           err_code = 414;
856 3           err = "URI Too Long\n";
857 3           goto got_bad_request;
858             }
859              
860             #if DEBUG >= 2
861             if (next_req_follows)
862             trace2("next req follows fd=%d, boff=%d\n",c->fd,body_offset);
863             if (body_is_required)
864             trace2("body is required fd=%d, boff=%d\n",c->fd,body_offset);
865             #endif
866              
867             // a body or follow-on data potentially follows the headers. Let feer_req
868             // retain its pointers into rbuf and make a new scalar for more body data.
869             STRLEN from_len;
870 621           char *from = SvPV(c->rbuf,from_len);
871             // Validate body_offset to prevent integer underflow
872             // Check for negative first (phr_parse_request returns -1/-2 for errors)
873 621 50         if (unlikely(body_offset < 0 || (STRLEN)body_offset > from_len)) {
    50          
874 0           trouble("invalid body_offset %d > from_len %"Sz_uf" fd=%d\n",
875             body_offset, (Sz)from_len, c->fd);
876 0           respond_with_server_error(c, "Internal parser error\n", 500);
877 0           return 0;
878             }
879 621           from += body_offset;
880 621           STRLEN need = from_len - body_offset;
881             trace("new rbuf for body %d need=%"Sz_uf"\n", c->fd, (Sz)need);
882 621 100         SV *new_rbuf = newSVpvn(need > 0 ? from : "", need);
883              
884 621           req->buf = c->rbuf;
885 621           c->rbuf = new_rbuf;
886 621           SvCUR_set(req->buf, body_offset);
887              
888             // determine how much we need to read
889 621           UV expected = 0;
890 621           bool got_host = 0;
891 621           bool got_transfer_encoding = 0;
892 2101 100         for (size_t i = 0; i < req->num_headers; i++) {
893 1529           struct phr_header *hdr = &req->headers[i];
894             // RFC 7230: reject obsolete header line folding (obs-fold)
895 1529 100         if (unlikely(!hdr->name)) {
896 1           err_code = 400;
897 1           err = "Obsolete header line folding not allowed\n";
898 1           goto got_bad_request;
899             }
900             // RFC 7231: reject header names that exceed our processing limit
901             // Buffer is 5 + MAX_HEADER_NAME_LEN, so names up to MAX_HEADER_NAME_LEN fit
902 1528 100         if (unlikely(hdr->name_len > MAX_HEADER_NAME_LEN)) {
903 2           err_code = 431;
904 2           err = "Header name too long\n";
905 2           goto got_bad_request;
906             }
907 1526 100         if (unlikely(hdr->name_len == 14 &&
    50          
908             str_case_eq_fixed("content-length", hdr->name, 14)))
909 77           {
910             // RFC 7230 3.3.3: reject if Transfer-Encoding was already seen
911 88 100         if (c->receive_chunked) {
912 3           err_code = 400;
913 3           err = "Content-Length not allowed with Transfer-Encoding\n";
914 11           goto got_bad_request;
915             }
916 85           UV new_expected = 0;
917 85           int g = grok_number(hdr->value, hdr->value_len, &new_expected);
918 85 100         if (likely(g == IS_NUMBER_IN_UV)) {
919 83 100         if (unlikely(new_expected > (UV)c->cached_max_body_len)) {
920 4           err_code = 413;
921 4           err = "Content length exceeds maximum\n";
922 4           goto got_bad_request;
923             }
924             // RFC 7230: reject multiple Content-Length with different values
925 79 100         if (got_content_length && new_expected != expected) {
    100          
926 2           err_code = 400;
927 2           err = "Multiple conflicting Content-Length headers\n";
928 2           goto got_bad_request;
929             }
930 77           expected = new_expected;
931 77           got_content_length = 1;
932             }
933             else {
934 2           err_code = 400;
935 2           err = "Invalid Content-Length\n";
936 2           goto got_bad_request;
937             }
938             }
939 1438 100         else if (unlikely(hdr->name_len == 10 &&
    100          
940             str_case_eq_fixed("connection", hdr->name, 10)))
941             {
942 415 100         if (c->is_http11 && c->is_keepalive &&
    100          
943 92 100         hdr->value_len == 5 && str_case_eq_fixed("close", hdr->value, 5))
    50          
944             {
945 83           c->is_keepalive = 0;
946             trace("setting conn %d to close after response\n", c->fd);
947             }
948 332 100         else if (!c->is_http11 && c->cached_keepalive_default &&
    50          
949 5 100         hdr->value_len == 10 && str_case_eq_fixed("keep-alive", hdr->value, 10))
    50          
950             {
951 1           c->is_keepalive = 1;
952             trace("setting conn %d to keep after response\n", c->fd);
953             }
954 415 100         if (next_req_follows && c->receive_chunked && (!c->is_http11 || got_host)) break;
    100          
    50          
    50          
955             }
956 1023 100         else if (unlikely(c->is_http11 && hdr->name_len == 6 &&
    100          
    100          
    100          
957             str_case_eq_fixed("expect", hdr->name, 6)))
958             {
959             // Check for "100-continue" value (case-insensitive)
960 35           if (hdr->value_len == 12 &&
961 13           str_case_eq_fixed("100-continue", hdr->value, 12))
962             {
963 13           c->expect_continue = 1;
964             trace("got Expect: 100-continue on fd=%d\n", c->fd);
965             }
966             else {
967             // RFC 7231: unknown expectation, respond with 417
968 9           err_code = 417;
969 9           err = "Expectation Failed\n";
970 9           goto got_bad_request;
971             }
972             }
973 1001 100         else if (unlikely(c->is_http11 && hdr->name_len == 17 &&
    100          
    100          
    100          
974             str_case_eq_fixed("transfer-encoding", hdr->name, 17)))
975 83           {
976             // RFC 7230 3.3.3: reject multiple Transfer-Encoding headers
977             // to prevent request smuggling attacks
978 103 100         if (got_transfer_encoding) {
979 6           err_code = 400;
980 6           err = "Multiple Transfer-Encoding headers not allowed\n";
981 6           goto got_bad_request;
982             }
983 97           got_transfer_encoding = 1;
984              
985             // RFC 7230: Accept "chunked" with optional extensions
986             // Valid formats: "chunked", "chunked;ext=val", "chunked ; ext"
987 95 100         bool is_chunked = (hdr->value_len >= 7 &&
988 192 100         str_case_eq_fixed("chunked", hdr->value, 7) &&
989 87 100         (hdr->value_len == 7 ||
990 3 100         hdr->value[7] == ';' ||
991 1 50         hdr->value[7] == ' ' ||
992 0 0         hdr->value[7] == '\t'));
993              
994             // Also accept "identity" which means no encoding
995 102           bool is_identity = (hdr->value_len == 8 &&
996 5           str_case_eq_fixed("identity", hdr->value, 8));
997              
998 97 100         if (is_chunked) {
999             // RFC 7230 3.3.3: reject if Content-Length is also present
1000             // This prevents request smuggling attacks
1001 87 100         if (got_content_length) {
1002 8           err_code = 400;
1003 8           err = "Content-Length not allowed with Transfer-Encoding\n";
1004 8           goto got_bad_request;
1005             }
1006 79           c->receive_chunked = 1;
1007             trace("got Transfer-Encoding: chunked on fd=%d\n", c->fd);
1008             }
1009 10 100         else if (is_identity) {
1010             // identity means no encoding - treat as if no TE header
1011             trace("got Transfer-Encoding: identity on fd=%d (ignored)\n", c->fd);
1012             }
1013             else {
1014             // Unsupported transfer encoding
1015 6           err_code = 501;
1016 6           err = "Unsupported Transfer-Encoding\n";
1017 6           goto got_bad_request;
1018             }
1019             }
1020 898 100         else if (unlikely(hdr->name_len == 4 &&
    50          
1021             str_case_eq_fixed("host", hdr->name, 4)))
1022             {
1023 622 100         if (unlikely(got_host)) {
1024 2           err_code = 400;
1025 2           err = "Duplicate Host header\n";
1026 2           goto got_bad_request;
1027             }
1028 620           got_host = 1;
1029             }
1030             }
1031              
1032             // RFC 7230 Section 5.4: HTTP/1.1 requests MUST include Host header
1033 576 100         if (unlikely(c->is_http11 && !got_host)) {
    100          
1034 1           err_code = 400;
1035 1           err = "Host header required for HTTP/1.1\n";
1036 1           goto got_bad_request;
1037             }
1038              
1039 575 100         if (c->cached_max_conn_reqs > 0 && c->reqs >= c->cached_max_conn_reqs) {
    100          
1040 1           c->is_keepalive = 0;
1041             trace("reached max requests per connection (%d), will close after response\n", c->cached_max_conn_reqs);
1042             }
1043              
1044             // RFC 9110 6.4.1: defense-in-depth against pipeline desync; reject
1045             // Transfer-Encoding on methods that have no defined request body (GET,
1046             // HEAD, DELETE, OPTIONS). Different proxies handle TE on body-less
1047             // methods inconsistently; rejecting keeps framing unambiguous.
1048 575 100         if (unlikely(c->receive_chunked && next_req_follows)) {
    100          
1049 4           err_code = 400;
1050 4           err = "Transfer-Encoding not allowed on this method\n";
1051 4           goto got_bad_request;
1052             }
1053              
1054 571 100         if (likely(next_req_follows) && !got_content_length && !c->receive_chunked) goto got_it_all;
    50          
    50          
1055 128 100         else if (likely(got_content_length)) goto got_cl;
1056 71 100         else if (unlikely(c->receive_chunked)) goto got_chunked;
1057              
1058             // body_is_required but no Content-Length or Transfer-Encoding
1059 4           err_code = 411;
1060 4           err = "Content-Length or Transfer-Encoding required\n";
1061              
1062 75           got_bad_request:
1063 75           respond_with_server_error(c, err, err_code);
1064 75           return 0;
1065              
1066 57           got_cl:
1067 57           c->expected_cl = (ssize_t)expected;
1068 57           c->received_cl = SvCUR(c->rbuf);
1069             trace("expecting body %d size=%"Ssz_df" have=%"Ssz_df"\n",
1070             c->fd, (Ssz)c->expected_cl, (Ssz)c->received_cl);
1071 57 50         SvGROW(c->rbuf, c->expected_cl + 1);
    100          
1072              
1073             // don't have enough bytes to schedule immediately?
1074             // unlikely = optimize for short requests
1075 57 100         if (unlikely(c->expected_cl && c->received_cl < c->expected_cl)) {
    100          
1076 13           send_100_continue(c);
1077 13           return 1;
1078             }
1079             // fallthrough: have enough bytes
1080 44           goto got_it_all;
1081              
1082 67           got_chunked:
1083 67           c->chunk_remaining = CHUNK_STATE_PARSE_SIZE;
1084 67           c->chunk_count = 0;
1085 67           c->trailer_count = 0;
1086 67           c->expected_cl = 0; /* accumulates as chunks are decoded */
1087 67           c->received_cl = 0;
1088 67           change_receiving_state(c, RECEIVE_CHUNKED);
1089             trace("starting chunked receive on fd=%d, have=%"Sz_uf" bytes\n",
1090             c->fd, (Sz)SvCUR(c->rbuf));
1091              
1092 67           send_100_continue(c);
1093              
1094             // Try to parse any chunks we already have
1095             {
1096 67           int ret = try_parse_chunked(c);
1097 67 100         if (ret == 1)
1098 6           return 1; // need more data
1099 61 100         if (ret == -1) {
1100 15           err_code = 400;
1101 15           err = "Malformed chunked encoding\n";
1102 15           goto got_bad_request;
1103             }
1104             }
1105             // fallthrough: chunked body complete
1106              
1107 46           got_it_all:
1108 533           sched_request_callback(c);
1109 533           return 0;
1110             }
1111              
1112             static void
1113 1394           conn_write_ready (struct feer_conn *c)
1114             {
1115             #ifdef FEERSUM_HAS_H2
1116             if (c->is_h2_stream) {
1117             h2_try_stream_write(aTHX_ c);
1118             return;
1119             }
1120             #endif
1121              
1122 1394 100         if (c->in_callback) {
1123             // Inside a callback: defer writes via the event loop write watcher.
1124             // This ensures data gets flushed after the callback returns.
1125 633           start_write_watcher(c);
1126 633           return;
1127             }
1128              
1129             #ifdef FEERSUM_HAS_TLS
1130 761 100         if (c->tls) {
1131             // Call TLS write path directly instead of deferring to write watcher.
1132             // This mirrors how plain HTTP calls try_conn_write immediately below.
1133 50           try_tls_conn_write(feersum_ev_loop, &c->write_ev_io, EV_WRITE);
1134 50           return;
1135             }
1136             #endif
1137              
1138             // attempt a non-blocking write immediately if we're not already
1139             // waiting for writability
1140 711           try_conn_write(feersum_ev_loop, &c->write_ev_io, EV_WRITE);
1141             }
1142              
1143             /* H2 dispatch shims for XS CODE/PPCODE blocks (which cannot use #ifdef). */
1144             static int
1145 73           h2_try_write_chunk (pTHX_ struct feer_conn *c, SV *body)
1146             {
1147             #ifdef FEERSUM_HAS_H2
1148             if (unlikely(c->is_h2_stream)) {
1149             feersum_h2_write_chunk(aTHX_ c, body);
1150             return 1;
1151             }
1152             #endif
1153             PERL_UNUSED_VAR(c); PERL_UNUSED_VAR(body);
1154 73           return 0;
1155             }
1156              
1157             static int
1158 14           h2_is_stream (struct feer_conn *c)
1159             {
1160             #ifdef FEERSUM_HAS_H2
1161             if (unlikely(c->is_h2_stream))
1162             return 1;
1163             #endif
1164             PERL_UNUSED_VAR(c);
1165 14           return 0;
1166             }
1167              
1168             INLINE_UNLESS_DEBUG static void
1169 80           send_100_continue (struct feer_conn *c)
1170             {
1171 80 100         if (likely(!c->expect_continue))
1172 67           return;
1173              
1174             static const char continue_response[] = "HTTP/1.1 100 Continue" CRLF CRLF;
1175             #ifdef FEERSUM_HAS_TLS
1176 13 100         if (c->tls) {
1177 2           feer_tls_send(c, continue_response, sizeof(continue_response) - 1);
1178 2           feer_tls_flush_wbuf(c);
1179 2 50         if (c->tls_wbuf.off > 0)
1180 0           start_write_watcher(c);
1181 2           c->expect_continue = 0;
1182 2           return;
1183             }
1184             #endif
1185 11           ssize_t wr = write(c->fd, continue_response, sizeof(continue_response) - 1);
1186             // If write fails with EAGAIN or is partial, client will timeout and
1187             // send body anyway (RFC 7231 recommends client wait ~1 second)
1188 11 50         if (likely(wr > 0)) {
1189             trace("sent 100 Continue to fd=%d\n", c->fd);
1190             }
1191 0 0         else if (wr < 0 && errno != EAGAIN && errno != EINTR) {
    0          
1192             trace("100 Continue write error fd=%d: %s\n", c->fd, strerror(errno));
1193             }
1194 11           c->expect_continue = 0; // only send once
1195             }
1196              
1197             INLINE_UNLESS_DEBUG static void
1198 790           free_feer_req (struct feer_req *req)
1199             {
1200 790 100         if (unlikely(!req))
1201 146           return;
1202              
1203             /* SvREFCNT_dec is NULL-safe — no need to guard each field. */
1204 644           SvREFCNT_dec(req->buf);
1205 644           SvREFCNT_dec(req->path);
1206 644           SvREFCNT_dec(req->query);
1207             #ifdef FEERSUM_HAS_H2
1208             SvREFCNT_dec(req->h2_method_sv);
1209             SvREFCNT_dec(req->h2_uri_sv);
1210             #endif
1211 644 50         FEER_REQ_FREE(req);
1212             }
1213              
1214             INLINE_UNLESS_DEBUG static void
1215 790           free_request (struct feer_conn *c)
1216             {
1217 790           free_feer_req(c->req);
1218 790           c->req = NULL;
1219 790           }
1220              
1221             static void
1222 214           respond_with_server_error (struct feer_conn *c, const char *msg, int err_code)
1223             {
1224             SV *tmp;
1225             STRLEN msg_len;
1226              
1227             #ifdef FEERSUM_HAS_H2
1228             if (unlikely(c->is_h2_stream)) {
1229             feersum_h2_respond_error(c, err_code);
1230             return;
1231             }
1232             #endif
1233              
1234 214 100         if (unlikely(c->responding != RESPOND_NOT_STARTED)) {
1235 1           trouble("Tried to send server error but already responding!");
1236 1           return;
1237             }
1238              
1239 213           msg_len = strlen(msg);
1240             assert(msg_len < INT_MAX);
1241              
1242 213           tmp = newSVpvf("HTTP/1.%d %d %s" CRLF
1243             "Content-Type: text/plain" CRLF
1244             "Connection: close" CRLF
1245             "Cache-Control: no-cache, no-store" CRLF
1246             "Content-Length: %"Ssz_df"" CRLFx2
1247             "%.*s",
1248             c->is_http11 ? 1 : 0,
1249             err_code, http_code_to_msg(err_code),
1250             (Ssz)msg_len,
1251             (int)msg_len, msg);
1252 213           add_sv_to_wbuf(c, sv_2mortal(tmp));
1253              
1254 213           stop_read_watcher(c);
1255 213           stop_read_timer(c);
1256 213           stop_header_timer(c); // Slowloris protection
1257 213           change_responding_state(c, RESPOND_SHUTDOWN);
1258 213           change_receiving_state(c, RECEIVE_SHUTDOWN);
1259 213           c->is_keepalive = 0;
1260 213           conn_write_ready(c);
1261             }
1262              
1263