# picohttpparser: zero-copy HTTP/1 parsing in 2 C files, no allocation

> picohttpparser is a stateless C HTTP parser that fills caller-allocated pointers into the input buffer without allocating memory. It handles HTTP/1 requests, responses and chunked decoding through 4 functions and powers HTTP/1 parsing in the H2O web server and multiple Perl web modules.

**h2o/picohttpparser** — tiny HTTP parser written in C (used in HTTP::Parser::XS et al.)

- Repository: https://github.com/h2o/picohttpparser
- Stars: 2,110 · Forks: 276
- Language: C
- License: not declared
- Published: 2026-10-11 · Updated: 2026-10-11 · Language: en
- Canonical page: https://hysenlabs.com/projects/h2o-picohttpparser

## 4 functions, no allocation, no state: the parsing scope of picohttpparser

picohttpparser exposes 4 functions: `phr_parse_request`, `phr_parse_response`, `phr_parse_headers` and `phr_decode_chunked`. Each function takes a pointer to the caller's buffer and pointers to output structures, then sets those output pointers to address the relevant portions of the buffer. No copy is made and no memory is allocated inside the library.

Stateless design means each call stands alone. A call to `phr_parse_request` either succeeds, fails, or returns -2 to indicate an incomplete request. All memory belongs to the caller: the input buffer, the output structures and the headers array. Neither malloc nor free is called by the library.

picohttpparser is the HTTP/1 parser inside the H2O web server. Plack, Starman, Starlet and Furl use it through the HTTP::Parser::XS binding, which wraps the C functions for use from Perl. Neither HTTP/2 nor TLS parsing is part of the library's scope; picohttpparser handles only the HTTP/1 byte-level parse.

## Adding picohttpparser to a C project: 2 source files, a Makefile and a test reference

picohttpparser ships as 2 source files. Adding the library to a project means copying picohttpparser.c and picohttpparser.h into the build. No package manager step is required.

A Makefile in the repository defines a test target that compiles picohttpparser.c, picotest/picotest.c and test.c into a test binary and runs it. CFLAGS is set to `-Wall -fsanitize=address,undefined` in that Makefile, enabling address sanitizer and undefined behaviour sanitizer during test runs. Both sanitizers are standard compiler flags for catching memory errors and undefined C behaviour.

Reading test.c is the fastest path to understanding the struct layout and the expected calling convention. The README points to test.c directly as the documentation for how to use the parser, so the test file is both the verification tool and the usage reference.

## phr_parse_request return codes: 3 outcomes and what each means for the caller

`phr_parse_request` returns an int with 3 distinct outcomes. A positive value is the number of bytes consumed by the complete request, confirming the parse succeeded. A return of -1 indicates a parse error. A return of -2 means the request is incomplete; the caller must read more bytes into the buffer and call the function again with the updated length.

```c
pret = phr_parse_request(buf, buflen, &method, &method_len, &path, &path_len,
                         &minor_version, headers, &num_headers, prevbuflen);
if (pret > 0)
    break; /* successfully parsed the request */
else if (pret == -1)
    return ParseError;
/* request is incomplete, continue the loop */
assert(pret == -2);
```

After a successful parse, method, path and each header name and value are pointers into the original buffer. Byte lengths (method_len, path_len, name_len, value_len) are set by the parser on success. No null terminators are added, so any string processing must use the length values to avoid reading past the end of each field. minor_version holds the HTTP minor version as an integer.

## Caller-allocated headers array: 100 entries in the example, no parser-side overflow guard

Calling `phr_parse_request` requires a pre-allocated headers array. In the README example, 100 entries are pre-allocated and the initial count is set before each call:

```c
struct phr_header headers[100];
num_headers = sizeof(headers) / sizeof(headers[0]);
```

Passing the count before the call tells the parser the maximum number of entries available. On success, the parser overwrites it with the actual count. An HTTP request with more than 100 headers would exceed the caller's array. No built-in protection exists against that case; sizing the array to match the expected request profile is the caller's responsibility.

Each phr_header entry has 4 fields: name, name_len, value and value_len. Both name and value point into the original input buffer with no null terminator. Integer fields hold the byte length of each string. Any code that copies or compares those fields must bound all reads by the length, not by scanning for a null byte.

## Incremental parsing: prevbuflen and the pret == -2 continuation loop

Incremental parsing follows a pattern where `prevbuflen` records the buffer length before each read. Both the total buffer length and the previous length are passed to `phr_parse_request` on every call, which lets the parser skip bytes it already examined in the previous iteration.

```c
char buf[4096], *method, *path;
size_t buflen = 0, prevbuflen = 0, method_len, path_len, num_headers;

while (1) {
    while ((rret = read(sock, buf + buflen, sizeof(buf) - buflen)) == -1 && errno == EINTR)
        ;
    if (rret <= 0)
        return IOError;
    prevbuflen = buflen;
    buflen += rret;
    num_headers = sizeof(headers) / sizeof(headers[0]);
    pret = phr_parse_request(buf, buflen, &method, &method_len, &path, &path_len,
                             &minor_version, headers, &num_headers, prevbuflen);
    if (pret > 0)
        break;
    else if (pret == -1)
        return ParseError;
    assert(pret == -2);
    if (buflen == sizeof(buf))
        return RequestIsTooLongError;
}
```

In this example, the buffer is fixed at 4096 bytes. When buflen reaches that size before the parse succeeds, the loop returns RequestIsTooLongError. That ceiling is a caller-side check; picohttpparser imposes no maximum request size of its own. After ruling out both success (pret > 0) and error (pret == -1), the only remaining value is -2, which is what `assert(pret == -2)` verifies at development time.

## phr_decode_chunked: in-place decoding and the consume_trailer field the zero-clear leaves at 0

`phr_decode_chunked` decodes chunked transfer encoding in-place, writing output into the same buffer that holds the encoded input. Return codes follow the same convention as phr_parse_request: a non-negative value means decoding is complete, -2 means more data is expected, and -1 means an error.

A `phr_chunked_decoder` struct controls the decoding and must be zero-cleared before use:

```c
struct phr_chunked_decoder decoder = {}; /* zero-clear */
decoder.consume_trailer = 1;
```

Zero-clearing sets consume_trailer to 0. In the example, that value is immediately overridden to 1. A code comment makes the consequence explicit: with consume_trailer at 0, the application must call phr_parse_headers to parse the trailing header after decoding finishes; setting it to 1 discards the trailing header instead. Any decoder struct that is zero-cleared without the explicit override leaves the trailing header unread after decoding completes, requiring a follow-up parse that is easy to omit. Placing the consume_trailer = 1 assignment directly after the zero-clear is the pattern the example establishes to avoid that requirement.

## Perl XS deployment, use in H2O, and the copyright year the files were not updated

picohttpparser reaches 2 different ecosystems from the same 2 source files. In Perl, HTTP::Parser::XS wraps the C functions for use from Perl without rewriting the parsing logic. On the C side, H2O uses picohttpparser as its HTTP/1 parser; both projects are in the same GitHub organisation.

What picohttpparser does not do is as important as what it does: no TLS, no HTTP/2, no connection keep-alive, no header validation beyond syntax, and no automatic buffer resizing. Those responsibilities sit in the layer above the library.

Dual Perl/MIT licensing is stated in both the README and the Makefile header; the last push was on 2026-09-29 and the repository is not archived. Copyright notices in both files read 2009-2014, but the repository received a push on 2026-09-29, more than twelve years after the copyright year ends, which means the copyright header was never updated to reflect continued activity.

## Conclusion

picohttpparser is the right choice when the caller already owns a socket buffer and needs fast, allocation-free HTTP/1 parsing without a framework. It is used directly in the H2O web server and reaches Perl applications through HTTP::Parser::XS. Skip it when the project needs HTTP/2, TLS, connection management, or automatic buffer resizing. Before embedding it, read test.c to understand the headers array sizing and set consume_trailer explicitly on the chunked decoder struct rather than relying on the zero default, since that default requires a follow-up phr_parse_headers call on trailing headers.

## FAQ

### What does picohttpparser parse?

picohttpparser parses HTTP/1 requests, responses and chunked transfer encoding. It exposes 4 functions: phr_parse_request, phr_parse_response, phr_parse_headers and phr_decode_chunked. It does not handle TLS, HTTP/2 or connection management.

### Does picohttpparser allocate memory?

No. picohttpparser does not allocate or free memory. The caller provides all buffers: the input buffer, the headers array and the output pointers. The parser sets those output pointers to address portions of the caller's input buffer.

### How do I add picohttpparser to a C project?

Add picohttpparser.c and picohttpparser.h to the project build. No package manager or separate install step is needed. The README points to test.c as the reference for how to call the functions.

### What does phr_parse_request return when the request is incomplete?

It returns -2 when the HTTP request is not yet complete. The caller should read more data into the buffer and call phr_parse_request again with the updated buffer length and the previous buffer length as the final argument. A return value greater than 0 means the full request was parsed.

### What is consume_trailer in phr_decode_chunked?

consume_trailer is a field in phr_chunked_decoder that controls how the trailing header after chunked data is handled. Setting it to 1 discards the trailing header. The default value after zero-clearing the struct is 0, which requires the caller to invoke phr_parse_headers on the trailing header data after decoding completes.

## Sources

- [h2o/picohttpparser on GitHub](https://github.com/h2o/picohttpparser)
- [Issues](https://github.com/h2o/picohttpparser/issues)
- [README](https://github.com/h2o/picohttpparser/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/h2o-picohttpparser
