Skip to content

Instantly share code, notes, and snippets.

@prenaissance
Created January 20, 2026 22:08
Show Gist options
  • Select an option

  • Save prenaissance/c9c13ba0aec45113c6043ea1bd932cd4 to your computer and use it in GitHub Desktop.

Select an option

Save prenaissance/c9c13ba0aec45113c6043ea1bd932cd4 to your computer and use it in GitHub Desktop.
Typescript Http protocol header parser
type Input = {
text: string;
index: number;
};
type ParseResult<T> =
| { success: true; value: T; remaining: Input }
| { success: false };
type InferParser<U> = U extends P<infer T> ? T : never;
class P<T> {
constructor(private parseFn: (input: Input) => ParseResult<T>) {}
parse(inputText: string): ParseResult<T> {
const input: Input = { text: inputText, index: 0 };
return this.parseFn(input);
}
private run(input: Input): ParseResult<T> {
return this.parseFn(input);
}
static char<C extends string>(c: C): P<C> {
return new P<C>((input) => {
if (input.index < input.text.length && input.text[input.index] === c) {
return {
success: true,
value: c,
remaining: { text: input.text, index: input.index + 1 },
};
}
return { success: false };
});
}
static string<S extends string>(str: S): P<S> {
return new P<S>((input) => {
if (input.text.slice(input.index, input.index + str.length) === str) {
return {
success: true,
value: str,
remaining: { text: input.text, index: input.index + str.length },
};
}
return { success: false };
});
}
static regex(regex: RegExp): P<string> {
return new P<string>((input) => {
const match = regex.exec(input.text.slice(input.index));
if (match && match.index === 0) {
const matchedString = match[0];
return {
success: true,
value: matchedString,
remaining: {
text: input.text,
index: input.index + matchedString.length,
},
};
}
return { success: false };
});
}
static seq<T1, T2>(parsers: [P<T1>, P<T2>]): P<[T1, T2]>;
static seq<T1, T2, T3>(parsers: [P<T1>, P<T2>, P<T3>]): P<[T1, T2, T3]>;
static seq<T1, T2, T3, T4>(
parsers: [P<T1>, P<T2>, P<T3>, P<T4>],
): P<[T1, T2, T3, T4]>;
static seq<T1, T2, T3, T4, T5>(
parsers: [P<T1>, P<T2>, P<T3>, P<T4>, P<T5>],
): P<[T1, T2, T3, T4, T5]>;
static seq(parsers: P<unknown>[]): P<unknown[]> {
return new P<unknown[]>((input) => {
const values: unknown[] = [];
let currentInput = input;
for (const parser of parsers) {
const result = parser.run(currentInput);
if (!result.success) {
return { success: false };
}
values.push(result.value);
currentInput = result.remaining;
}
return { success: true, value: values, remaining: currentInput };
});
}
or<U>(other: P<U>): P<T | U> {
return new P<T | U>((input) => {
const result = this.run(input);
if (result.success) {
return result;
}
return other.run(input);
});
}
map<U>(fn: (value: T) => U): P<U> {
return new P<U>((input) => {
const result = this.run(input);
if (result.success) {
return {
success: true,
value: fn(result.value),
remaining: result.remaining,
};
}
return { success: false };
});
}
}
const method = P.string("GET")
.or(P.string("POST"))
.or(P.string("PUT"))
.or(P.string("PATCH"))
.or(P.string("DELETE"));
const space = P.char(" ");
const path = P.regex(/\/[a-zA-Z0-9.-~/]*/);
const version = P.string("1.1").or(P.string("2")).or(P.string("3"));
const httpVersion = P.seq([P.string("HTTP/"), version]).map(([, ver]) => ver);
const requestLine = P.seq([method, space, path, space, httpVersion]).map(
([method, , path, , version]) => ({ method, path, version }),
);
// biome-ignore lint/correctness/noUnusedVariables: example
type ParsedHttpRequestLine = InferParser<typeof requestLine>;
const result = requestLine.parse("GET /api/profile/avatar.webp HTTP/2");
console.log(result);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment