{"record":{"id":"cc60b7a6a2b7719e","repo":"cloudflare/pingora","slug":"body-buf-exists-once-a-partial-chunk-head-was-buff","errorCode":null,"errorMessage":"body buf exists once a partial chunk head was buffered","messagePattern":"body buf exists once a partial chunk head was buffered","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pingora-core/src/protocols/http/v1/body.rs","lineNumber":522,"sourceCode":"                                .or_err(ReadError, \"when reading body\")?;\n                        }\n                    } else {\n                        /* existing_buf_end != 0 this is partial chunk head */\n                        /* copy the #expecting_from_io bytes until index existing_buf_end\n                         * to the front and read more to form a valid chunk head.\n                         * existing_buf_end is the end of the partial head and\n                         * expecting_from_io is the len of it.\n                         * Unlike the pipeline handoff this compacts in place rather than\n                         * split_off()-ing at the head. The head is capped at\n                         * PARTIAL_CHUNK_HEAD_LIMIT and is normally a few bytes, so the move is\n                         * bounded and cheap, while split_off() would leave only\n                         * capacity - partial_head_start of room behind it and force the reserve\n                         * below to move the same bytes anyway (or reallocate) to fit the read. */\n                        self.reserve_buf_for_io();\n                        let body_buf = self\n                            .body_buf\n                            .as_mut()\n                            .expect(\"body buf exists once a partial chunk head was buffered\");\n                        let partial_head_start = existing_buf_end - expecting_from_io;\n                        if partial_head_start != 0 {\n                            body_buf.copy_within(partial_head_start..existing_buf_end, 0);\n                        }\n                        body_buf.truncate(expecting_from_io);\n                        // Commit the compacted offsets before awaiting so cancellation leaves the\n                        // parser and buffer in a state that the next call can safely resume.\n                        self.body_state = self\n                            .body_state\n                            .partial_chunk_head(expecting_from_io, expecting_from_io);\n                        let new_bytes = stream\n                            .take((self.body_buf_size - expecting_from_io) as u64)\n                            .read_buf(body_buf)\n                            .await\n                            .or_err(ReadError, \"when reading body\")?;\n                        if new_bytes == 0 {\n                            self.body_state = self.body_state.done(0);\n                            return Error::e_explain(","sourceCodeStart":504,"sourceCodeEnd":540,"githubUrl":"https://github.com/cloudflare/pingora/blob/0046038bd402bc82912da862dadf9a479f31e9f1/pingora-core/src/protocols/http/v1/body.rs#L504-L540","documentation":"During chunked transfer-encoding parsing, when only a partial chunk header was buffered, do_read_chunked_body compacts the partial head in place and .expect()s the body buffer to exist ('body buf exists once a partial chunk head was buffered', body.rs:522). The invariant is that buffering a partial head implies body_buf was allocated; the panic means the reader entered the partial-head state with no buffer — an internal parser state violation.","triggerScenarios":"A chunked HTTP/1.1 upstream response whose chunk-size line arrives split across TCP segments in a way that drives the parser into PartialChunkHead state without the expected buffer, or library misuse that desynchronizes body_state from body_buf. Fires mid-body while proxying.","commonSituations":"Malformed or adversarial chunked responses (fuzzers, broken origin servers); version upgrades that changed parser state transitions; concurrent manipulation of the session between awaits.","solutions":["Upgrade pingora-core to the newest release; chunked-parser invariant panics are treated as bugs","Reproduce with the exact origin bytes: log the upstream response via a capture proxy and attach it to a pingora issue","As containment, route the offending origin through a non-chunked path (disable chunked upstream or terminate at an intermediary) while the fix lands"],"exampleFix":"# before\npingora-core = \"0.7\"\n\n# after\ncargo update -p pingora-core  # pick up chunked parser fixes","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Isolate per-connection parsing; a panic becomes a 502/reset for that stream only\nlet task = tokio::spawn(conn_handler(stream));\nif task.await.unwrap_err().is_panic() {\n    log::error!(\"chunked parser panicked for peer {peer}\"); // keep the proxy alive\n}","preventionTips":["Fuzz chunked transfer-encoding inputs (split chunk heads across segment boundaries) in CI","Do not share or manually reset session body buffers from custom filters while parsing is in flight","Monitor panic counts per release; regressions in H1 parsing show up as spikes"],"tags":["rust","http1","body-parser","chunked-encoding","pingora","invariant","panic"],"backgroundTag":"http-body-parser-panic","analyzedSha":"0046038bd402bc82912da862dadf9a479f31e9f1","analyzedAt":"2026-08-16T21:33:22.341Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}