Repository navigation
2 cshttp web Toolkit
Source Folder:
src/cshttp/Web/
Namespace:cshttp
Dependencies: References onlycshttp(Zero external NuGet packages, C# 7.3,.NET Framework 4.8and.NET 8 / 10+)
cshttp.web sits directly above the cshttp wire parser and provides the complete application-facing HTTP object model and content toolkit.
It serves two simultaneous goals:
-
Lazy, High-Performance Content Parsing: URL percent-decoding, query strings (
application/x-www-form-urlencoded),multipart/form-datafile uploads,Cookie/Set-Cookieheaders,Cache-Controlpolicies, and range-capable static file serving are evaluated lazily on first access. If a request handler only checksRequest.Pathand writes a JSON response, it pays zero CPU or memory allocation cost for parsing cookies, query strings, or form bodies. -
Familiar
System.Web-Style API: The core request/response surface (HttpContext.Current,HttpRequest,HttpResponse,HttpCookie,HttpPostedFile,HttpUtility,HostingEnvironment.MapPath) follows classicSystem.Webnames and shapes, so Pageless ASP.NET handlers port with little effort. It is not a 1:1 mirror: Web Forms, Session, modules, and authentication are out of scope, and some members deliberately differ (e.g.TransmitFile(path)is a range- and cache-aware static file sender, andWriteFileis not provided).
Because cshttp.web writes responses through a 3-method interface (IHttpResponseSink), you can use cshttp + cshttp.web without cshttp.server—for example, to unit-test request handlers in memory at 100,000+ calls/sec or to run System.Web-style handlers over custom transports (named pipes, custom sockets, or serverless adapters).
| File | Public Types | Role |
|---|---|---|
| HttpContext.cs |
HttpContext, HttpApplication, HttpServerUtility
|
Ambient HttpContext.Current (AsyncLocal<HttpContext>), per-request Items, Server.MapPath, and CompleteRequest(). |
| HttpRequest.cs |
HttpRequest, UnvalidatedRequestValues
|
Lazy-parsed Path, QueryString, Form, Files, Cookies, Headers, BodyText, BodyBytes, InputStream, ServerVariables, Params, and unified Request["key"] indexer. |
| HttpResponse.cs |
HttpResponse, IHttpResponseSink
|
Buffered (Content-Length) or streaming (Transfer-Encoding: chunked via Flush() / BufferOutput = false) response writer and TransmitFile, which streams files from disk in 64 KB chunks at send time (never loading the whole file into memory). |
| CookiesAndFiles.cs |
HttpCookie, HttpCookieCollection, SameSiteMode, HttpPostedFile, HttpFileCollection
|
RFC 6265 cookies (with HttpOnly, Secure, SameSite) and zero-copy HttpPostedFile slices over the request body buffer. |
| MultipartParser.cs | MultipartParser |
RFC 7578 multipart/form-data parser supporting multiple file uploads, RFC 8187 filename* UTF-8 decoding, and part-count limits. |
| UrlEncoding.cs |
PercentDecoder, UrlEncodedParser
|
Single-pass percent-decoder and application/x-www-form-urlencoded parser (operates directly on byte[] or string). |
| HttpUtility.cs | HttpUtility |
HtmlEncode, HtmlAttributeEncode, HtmlDecode, UrlEncode, UrlPathEncode, UrlDecode, ParseQueryString, and JavaScriptStringEncode. |
| HttpCachePolicy.cs |
HttpCachePolicy, HttpCacheability
|
Response.Cache builder (SetCacheability, SetMaxAge, SetNoStore, SetETag, SetLastModified). |
| StaticFiles.cs |
StaticFiles, StaticFileOptions, MimeTypes
|
Production static file server with ETag, Last-Modified, 304 Not Modified, 206 Partial Content byte ranges, ?v= immutable cache-busting, and strict path-traversal guards. |
| HostingEnvironment.cs | HostingEnvironment |
ApplicationPhysicalPath, MapPath("~/...") with root-escape protection, and graceful background work tracking (accessible via WebApp.BackgroundWork). |
| Common.cs |
HttpException, HttpResponseEndException, RequestSettings, RequestConnectionInfo, HttpValueCollection, HeaderValue
|
Status-code exceptions, body-parsing limits (MaxCollectionKeys = 1000), and proxy-aware connection metadata. |
When an HttpRequest is constructed from an HttpRequestHead and body byte[], nothing is parsed upfront. Each collection is materialized only when your handler touches it:
HttpRequest Request = HttpContext.Current.Request;
// 1. URL & Path (RFC 3986 percent-decoded)
string method = Request.HttpMethod; // "POST"
string path = Request.Path; // "/files/my report.pdf"
string rawUrl = Request.RawUrl; // "/files/my%20report.pdf?folder=docs"
// 2. Lazy-parsed collections
string folder = Request.QueryString["folder"]; // WHATWG / System.Web urlencoded query
string title = Request.Form["title"]; // Parsed only if Content-Type is urlencoded or multipart
string sid = Request.Cookies["ssid"]?.Value;
// 3. Unified System.Web Indexer (searches QueryString -> Form -> Cookies -> ServerVariables)
string action = Request["action"];
// 4. Zero-Copy Multipart File Uploads (RFC 7578)
HttpPostedFile upload = Request.Files["document"];
if (upload != null && upload.ContentLength > 0)
{
string safeName = System.IO.Path.GetFileName(upload.FileName);
upload.SaveAs(HostingEnvironment.MapPath("~/App_Data/uploads/" + safeName));
// Or read directly from upload.InputStream without copying the underlying byte[] buffer!
}
// 5. Raw Request Body (Lazy-decoded text, cached bytes, or zero-copy stream)
string payload = Request.BodyText; // Decoded once using ContentEncoding (UTF-8 default)
byte[] bytes = Request.BodyBytes; // Cached raw byte array
Stream stream = Request.InputStream; // Zero-copy MemoryStream directly over the receive buffer
// 6. Client IP & Proxy Resolution (populated after TrustedProxies validation)
string clientIp = Request.UserHostAddress;
bool isHttps = Request.IsSecureConnection;
bool isLocal = Request.IsLocal;HttpResponse automatically manages HTTP/1.1 framing so handlers never have to calculate Content-Length manually:
-
Buffered Mode (Default —
BufferOutput = true):-
Response.Write(...)/Response.BinaryWrite(...)data is queued in 16 KB memory segments; eachResponse.TransmitFile(...)call is queued as a single file segment (path + byte range) and read from disk only at send time. - If an exception is thrown before
Flush(), the server can cleanlyResponse.Reset()and emit a500or400error page. - When the handler returns,
response.Complete()sums the exact byte length of all memory segments and file ranges, emitsContent-Length: N, and streams the response over the keep-alive connection.
-
-
Streaming Chunked Mode (
Response.Flush()orBufferOutput = false):- Calling
Response.Flush()mid-handler (or settingResponse.BufferOutput = false) immediately sends the HTTP headers withTransfer-Encoding: chunkedand wraps each subsequent write in RFC 9112 chunk framing ({hex}\r\n...\r\n), finishing with0\r\n\r\nwhen the handler completes.
- Calling
-
Safe
Response.End()withoutThreadAbortException:- In classic IIS,
Response.End()throwsThreadAbortException, which is expensive and uncatchable. Incshttp.web,Response.End()flushes the response, marks_ended = true(ignoring any subsequent writes even if a usercatch (Exception)block swallows the exception), and throws lightweightHttpResponseEndException.
- In classic IIS,
| Overload | Behavior |
|---|---|
bool TransmitFile(string path) |
Smart file sender for a file your code chose. Sets Content-Type from the extension, emits ETag / Last-Modified / Accept-Ranges, answers 304, and supports resumable downloads (Range / If-Range → 206 / 416). Only GET / HEAD are allowed (others get 405). Returns false (sending nothing) if the file is missing, hidden/system, or has an unknown extension. Cache-Control is public, max-age=3600, or public, max-age=31536000, immutable when the URL has ?v=. |
void TransmitFile(string path, long offset, long length) |
Raw sender: queues exactly that byte range (length = -1 means to end of file); no headers, no conditional or range logic. Throws FileNotFoundException if missing. |
If the handler has already set ContentType explicitly (or already written body data), TransmitFile(path) skips the smart logic and sends the whole file raw — use this for dynamic downloads such as generated reports, where public caching and the 405 check are not wanted.
Files are streamed from disk in 64 KB chunks at send time; the whole file is never loaded into memory. WriteFile is not provided — use TransmitFile.
StaticFiles.TryServe is the folder-serving route (URL → safe file path → policy), whereas Response.TransmitFile sends a file your code already chose. Use TryServe whenever the file path comes from the request URL. When running standalone without Nginx (or in development/LAN deployments), StaticFiles.TryServe serves static folders with full HTTP semantics and hardened security:
// Simple one-liner inside your request handler:
if (StaticFiles.TryServe(ctx, "/css/", "css")) return;
if (StaticFiles.TryServe(ctx, "/js/", "js")) return;
// Or with custom StaticFileOptions:
var opts = new StaticFileOptions("/assets/", "wwwroot/assets")
{
MaxAge = TimeSpan.FromHours(1),
ImmutableWhenVersioned = true, // ?v=20261020 -> "public, max-age=31536000, immutable"
VersionQueryKey = "v",
UseValidators = true, // Emits ETag & Last-Modified; answers 304 Not Modified
ServeUnknownFileTypes = false // Blocks .config, .cs, .db, etc. (404 like IIS)
};
if (StaticFiles.TryServe(ctx, opts)) return;Built-in StaticFiles Security & Protocol Features:
-
Path Traversal Hardening: Rejects
.., backslashes (\), drive colons (:), NUL bytes (%00), control characters, trailing dots/spaces (which Windows NTFS silently strips), and hidden/system files. Verified against 13 path-traversal attack vectors inServerTests.cs. -
Byte-Range Streaming (
206 Partial Content/416 Range Not Satisfiable): Supports video/audio scrubbing and resumable downloads (Range: bytes=0-1023,If-Range), streaming directly from disk in 64 KB chunks viaTransmitFile. -
Per-Folder Policy: Unlike
TransmitFile(path),TryServehonorsStaticFileOptions(MaxAge,MaxAgeOverride,VersionQueryKey,UseValidators,ServeUnknownFileTypes/DefaultContentType,PassThroughWhenMissing). Missing files return404(orfalsewithPassThroughWhenMissing), and requests outside the prefix returnfalseso routing can continue.
Because HttpResponse writes to IHttpResponseSink, you can invoke any cshttp.web handler in-memory without opening a TCP port:
using System;
using System.IO;
using System.Text;
using cshttp;
sealed class MemorySink : IHttpResponseSink
{
public readonly MemoryStream Stream = new MemoryStream();
public void Write(byte[] buffer, int offset, int count) => Stream.Write(buffer, offset, count);
public void Flush() { }
public bool IsConnected => true;
}
// Construct a synthetic request and run your handler directly in memory:
byte[] raw = Encoding.ASCII.GetBytes("GET /echo?name=Adrian HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpRequestHead head;
HttpRequestParser.ParseHead(raw, 0, raw.Length, null, out head);
var sink = new MemorySink();
var ctx = new HttpContext(
new HttpRequest(head, null, 0, 0, null, null),
new HttpResponse(sink, head));
HttpContext.Current = ctx;
try
{
ctx.Response.Write("Hello " + ctx.Request.QueryString["name"]);
ctx.Response.Complete();
}
finally
{
HttpContext.Current = null;
}
string wireOutput = Encoding.UTF8.GetString(sink.Stream.ToArray());Technical Specs
14 Days Tutorial