{"id":32037138,"url":"https://github.com/samicpp/dotnet-http","last_synced_at":"2026-04-18T19:32:04.182Z","repository":{"id":318746646,"uuid":"1074695120","full_name":"samicpp/dotnet-http","owner":"samicpp","description":"A HTTP/0.9, HTTP/1.1, WebSocket, HTTP/2, and HPACK library written completely in c#, with no dependencies","archived":false,"fork":false,"pushed_at":"2026-01-13T20:37:08.000Z","size":197,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-13T22:26:56.492Z","etag":null,"topics":["csharp","csharp-code","csharp-library","dotnet","http","http-framework","http-library","http-protocol","http09","http1","http2","http2-library","http2-protocol","websocket","websocket-framework"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/samicpp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-12T09:50:57.000Z","updated_at":"2026-01-13T20:37:12.000Z","dependencies_parsed_at":"2025-10-15T00:59:31.054Z","dependency_job_id":"b30ca80d-e32d-4686-a2db-49d23a6de556","html_url":"https://github.com/samicpp/dotnet-http","commit_stats":null,"previous_names":["samicpp/dotnet-http"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/samicpp/dotnet-http","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samicpp%2Fdotnet-http","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samicpp%2Fdotnet-http/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samicpp%2Fdotnet-http/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samicpp%2Fdotnet-http/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/samicpp","download_url":"https://codeload.github.com/samicpp/dotnet-http/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samicpp%2Fdotnet-http/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31982553,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T17:30:12.329Z","status":"ssl_error","status_checked_at":"2026-04-18T17:29:59.069Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","csharp-code","csharp-library","dotnet","http","http-framework","http-library","http-protocol","http09","http1","http2","http2-library","http2-protocol","websocket","websocket-framework"],"created_at":"2025-10-17T09:00:52.244Z","updated_at":"2026-04-18T19:32:04.174Z","avatar_url":"https://github.com/samicpp.png","language":"C#","readme":"# dotnet-http\nA http library in c# \u003cbr/\u003e \u003cbr/\u003e\n\nThis library is a low-level RFC implementation of HTTP. \nIt is expected that you implement logic like content serving yourself. \nYou can consult the [test code](/tests/Program.cs) for code examples/usage. \u003cbr/\u003e \u003cbr/\u003e\n\nThis library contains both sync and async methods allowing you to use it in both contexts. \nFor every blocking/waiting method theres an async/sync version. \u003cbr/\u003e \u003cbr/\u003e\n\nH2C and WebSocket upgrades can in the library. \u003cbr/\u003e \u003cbr/\u003e\n\nIn the future I might make another library that uses this one to support a ready to use Web framework with middleware and more.\n\n## TODO::Features\n- [x] allow both sync and async code\n- [x] implement HTTP/0.9 (utterly useless, for educational purposes only)\n- [x] implement HTTP/1.1\n- [x] implement WebSocket\n- [x] implement HPACK\n- [x] implement HTTP/2\n- [ ] implement QUIC\n- [ ] implement QPACK\n- [ ] implement HTTP/3\n\n## TODO::Improvements\n- [x] improve compressor\n- [x] support use of `Stream` instead of `byte[]`\n- [x] support trailing headers\n- [ ] support HTTP/1.1 pipelining\n- [ ] add comments/documentation\n- [ ] change client data type from List\u003cbyte\u003e to MemoryStream\n\n## Examples\n\nHere is the HTTP echo server example from the [test code](/tests/Program.cs).\n\nHTTP/1.1 echo server\n```c#\nusing Samicpp.Http;\nusing Samicpp.Http.Http1;\n\nusing System;\nusing System.Net;\nusing System.Text;\nusing System.Net.Sockets;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\n\npublic class Program\n{\n    public async Task Main()\n    {\n        // creating listener\n        IPEndPoint address = new(IPAddress.Parse(\"0.0.0.0\"), 2048);\n        using Socket listener = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n\n        // start listener\n        listener.Bind(address);\n        listener.Listen(10);\n\n        Console.WriteLine(\"http echo server listening on http://127.0.0.1:2048\");\n\n        // connection loop\n        while (true)\n        {\n            var shandler = await listener.AcceptAsync();\n            Console.WriteLine($\"\\e[32m{shandler.RemoteEndPoint}\\e[0m\");\n\n            var _ = Task.Run(async () =\u003e\n            {\n                // first we need to convert it to something we can pass to Samicpp.Http.TcpSocket\n                using NetworkStream stream = new(shandler, ownsSocket: true);\n\n                // then we use it to construct `Samicpp.Http.Http1.Http1Socket`\n                using Http1Socket socket = new(new TcpSocket(stream)); \n                // interface `Samicpp.Http.IDualHttpSocket` can also be used as data type, since the class implements this.\n                // individual H2 streams also implement this\n\n                Console.WriteLine(\"constructed protocol handler\");\n\n                // when the client uses `Transfer-Encoding: chunked` each read will only add 1 chunk to the body buffer\n                // if `Content-Length: n` was provided the library will only read the full body on the second read invocation \n                // to ensure not enforcing body read\n                // this is also usefull for Http2Streams where reading client doesnt block\n                var client = await socket.ReadClientAsync();\n\n                // ensures full client has been read\n                while (!client.HeadComplete || !client.BodyComplete) client = await socket.ReadClientAsync();\n\n                // the framework allows for headers to appear multiple times\n                if (client.Headers.TryGetValue(\"accept-encoding\", out List\u003cstring\u003e encoding))\n                {\n                    foreach (string s in encoding[0].Split(\",\"))\n                    {\n                        // setting `Samicpp.Http.IDualSocket.Compression` automatically ensures the appropriate compression type is used\n                        // the framework does not verify if client accepts the encoding, this was done on purpose to give the code full \n                        // the framework also doesnt set a Content-Encoding header\n                        switch(s)\n                        {\n                            case \"gzip\":\n                                socket.Compression = Compression.Gzip;\n                                socket.SetHeader(\"Content-Encoding\", \"gzip\");\n                                break;\n                            \n                            case \"deflate\":\n                                socket.Compression = Compression.Deflate;\n                                socket.SetHeader(\"Content-Encoding\", \"deflate\");\n                                break;\n\n                            case \"br\":\n                                socket.Compression = Compression.Brotli;\n                                socket.SetHeader(\"Content-Encoding\", \"br\");\n                                break;\n\n                            default:\n                                socket.Compression = Compression.None;\n                                socket.SetHeader(\"Content-Encoding\", \"identity\");\n                                break;\n                        };\n                        if (socket.Compression != Compression.None) break;\n                    }\n                    Console.WriteLine(\"using compression \" + socket.Compression);\n                }\n                else\n                {\n                    socket.SetHeader(\"Content-Encoding\", \"identity\");\n                    Console.WriteLine(\"no compression\");\n                }\n\n                Console.WriteLine(client);\n                Console.WriteLine($\"received {client.Body.Count} bytes\");\n\n                // the server doesnt decode the client body automatically, it also doesnt decompress it. this is the code's responsibility.\n                // for decompression you can use `Samicpp.Http.Compressor.Decompress`\n                var text = Encoding.UTF8.GetString([.. client.Body]);\n\n                Console.WriteLine($\"received request with body[{text.Length}] \\e[36m{text.Trim()}\\e[0m\");\n\n                // the server does ensure you cannot attempt to send data after connection has been closed\n                // nor does it allow you to send headers after\n                await socket.CloseAsync(Encoding.UTF8.GetBytes($\"\u003c| {text.Trim()} |\u003e\\n\"));\n            });\n        }\n    }\n}\n```\n\nHTTP/2 echo server\n```c#\nusing Samicpp.Http;\nusing Samicpp.Http.Http2;\n\nusing System;\nusing System.Net;\nusing System.Text;\nusing System.Net.Sockets;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\n\npublic class Program\n{\n    public async Task Main()\n    {\n        // creating listener\n        IPEndPoint address = new(IPAddress.Parse(\"0.0.0.0\"), 2048);\n        using Socket listener = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);\n\n        // start listener\n        listener.Bind(address);\n        listener.Listen(10);\n\n        Console.WriteLine(\"http echo server listening on http://127.0.0.1:2048\");\n\n        // connection loop\n        while (true)\n        {\n            var shandler = await listener.AcceptAsync();\n            Console.WriteLine($\"\\e[32m{shandler.RemoteEndPoint}\\e[0m\");\n\n            var _ = Task.Run(async () =\u003e\n            {\n                // first we need to convert it to something we can pass to class Samicpp.Http.TcpSocket\n                using NetworkStream stream = new(shandler, ownsSocket: true);\n\n                // then we use it to construct `Samicpp.Http.Http2.Http2Session`\n                using Http2Session h2 = new(new TcpSocket(stream), Http2Settings.Default()); \n                \n\n                // the framework doesnt automatically read and check the preface, so we have to invoke it manually\n                await h2.InitAsync();\n\n                Console.WriteLine(\"initialized http2 connection\");\n\n                // we also need to send our settings\n                await h2.SendSettingsAsync(Http2Settings.Default());\n                // by default it uses the constructor provided settings for the encoder header table size\n                // if you change this you will need to modify this manually with\n                // `Samicpp.Http.Http2.Http2Session.hpacke.TableSize = 4096`\n\n                // when the library handles a Goaway frame it will store this in `Samicpp.Http.Http2.Http2Session.goaway`\n                // we can use this as an indicator for open connections\n                while(h2.goaway == null)\n                {\n                    // first you need to read a http2 frame\n                    // avoid using ReadAllAsync\n                    Http2Frame frame = await socket.ReadAsync();\n                    // this reads all available buffer, which can contain incomplete frames\n                    // it is recommended you use `Samicpp.Http.Http2.Http2Session.ReadOneAsync` which does wait until it receives a whole frame\n                    // you can handle these manually if you want, but that is not necessary\n\n                    // we then pass the frame to the handler, which automatically updates stream states and more\n                    int? openedStream = await socket.HandleAsync(frame);\n                    // this returns a stream id if a stream was opened \n                    // this method also has an overload for multiple frames which returns `List\u003cint\u003e`\n\n\n                    foreach (int streamID in openedStreams)\n                    {\n                        var _ = Task.Run(async () =\u003e \n                        {\n                            // we can directly use `Samicpp.Http.Http2.Http2Session` to send headers/data but that is not necessary\n                            // we can use the single-stream handler `Samicpp.Http.Http2.Http2Stream`\n                            using Http2Stream stream = new(streamID, h2);\n                            // this implements `Samicpp.Http.IDualHttpSocket` allowing for interopibility \n                            // with functions that accept both Samicpp.Http.Http1.Http1Socket and `Samicpp.Http.Http2.Http2Stream`\n\n                            // in h2 it is much more realistic that not the whole client has been read\n                            // furthermore the library doesnt block for reading the client\n                            // it queries the stream state to retrieve client data\n                            var client = await stream.ReadClientAsync();\n                            while (!client.HeadComplete || !client.BodyComplete) client = await stream.ReadClientAsync();\n\n                            \n                            // we can use `Samicpp.Http.Http2.Http2Stream` like it is `Samicpp.Http.Http1.Http1Socket`\n                            stream.SetHeader(\"content-type\", \"text/plain\");\n\n                            // the client body is also of type `List\u003cbyte\u003e`\n                            var text = Encoding.UTF8.GetString([.. client.Body]);\n\n                            // `Samicpp.Http.Http2.Http2Stream.CloseAsync` and its sync version both include header `Content-Length` \n                            // in the response if the headers havent yet been sent\n                            await stream.CloseAsync(Encoding.UTF8.GetBytes($\"\u003c| {text.Trim()} |\u003e\\n\"));\n                        });\n                    }\n                }\n            });\n        }\n    }\n}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamicpp%2Fdotnet-http","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamicpp%2Fdotnet-http","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamicpp%2Fdotnet-http/lists"}