Last active
March 10, 2026 07:27
-
-
Save jonas1ara/58c88c96e010f3ea16af07d26a9b4696 to your computer and use it in GitHub Desktop.
Simple web server in F#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| open System.Net | |
| open System.Net.Sockets | |
| open System.IO | |
| open System.Text.RegularExpressions | |
| open System.Text | |
| /// A table of MIME content types. | |
| let mimeTypes = | |
| dict [".html", "text/html"; | |
| ".htm", "text/html"; | |
| ".txt", "text/plain"; | |
| ".gif", "image/gif"; | |
| ".jpg", "image/jpeg"; | |
| ".png", "image/png"] | |
| /// Compute a MIME type from a file extension. | |
| let getMimeType(ext) = | |
| if mimeTypes.ContainsKey(ext) then mimeTypes.[ext] | |
| else "binary/octet" | |
| /// The pattern Regex1 uses a regular expression to match one element. | |
| let (|Regex1|_|) (patt : string) (inp : string) = | |
| try Some(Regex.Match(inp, patt).Groups.Item(1).Captures.Item(0).Value) | |
| with _ -> None | |
| /// The root for the data we serve | |
| let root = @"~/wsf/wwwroot" | |
| /// Handle a TCP connection for an HTTP GET. We use an asynchronous task in | |
| /// case any future actions in handling a request need to be asynchronous. | |
| let handleRequest(client : TcpClient) = async { | |
| use stream = client.GetStream() | |
| let out = new StreamWriter(stream) | |
| let headers (lines : seq<string>) = | |
| let printLine s = s |> fprintf out "%s\r\n" | |
| lines |> Seq.iter printLine | |
| // An empty line is required before content, if any. | |
| printLine "" | |
| out.Flush() | |
| let notFound () = headers ["HTTP/1.0 404 Not Found"] | |
| let inp = new StreamReader(stream) | |
| let request = inp.ReadLine() | |
| match request with | |
| | "GET / HTTP/1.0" | "GET / HTTP/1.1" -> | |
| // From the root, redirect to the start page. | |
| headers ["HTTP/1.0 302 Found"; "Location: http://localhost:8090/iisstart.htm"] | |
| | Regex1 "GET /(.*?) HTTP/1\\.[01]$" fileName -> | |
| let fname = Path.Combine(root, fileName) | |
| let mimeType = getMimeType(Path.GetExtension(fname)) | |
| if not(File.Exists(fname)) then notFound() | |
| else | |
| let content = File.ReadAllBytes fname | |
| ["HTTP/1.0 200 OK"; | |
| sprintf "Content-Length: %d" content.Length; | |
| sprintf "Content-Type: %s" mimeType] | |
| |> headers | |
| stream.Write(content, 0, content.Length) | |
| | _ -> notFound()} | |
| /// The server as an asynchronous process. We handle requests sequentially. | |
| let server = async { | |
| let socket = new TcpListener(IPAddress.Parse("127.0.0.1"), 8090) | |
| socket.Start() | |
| while true do | |
| use client = socket.AcceptTcpClient() | |
| do! handleRequest client} | |
| Async.RunSynchronously server |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WebServer.fsx
A minimal HTTP/1.x web server implemented in a single F# script — no frameworks, no NuGet packages, just raw sockets and the .NET standard library.
dotnet fsi WebServer.fsx
What it does
WebServer.fsxlistens on127.0.0.1:8090and handles HTTP GET requests by serving static files from a configurable root directory. It uses F#'s async workflows to keep the connection handling non-blocking and composable.Features
TcpListener— noHttpListener, no ASP.NET/→/iisstart.htm) via HTTP 302async { }) for the server loop and each request handlerRegex1) for clean, declarative URL parsingHow it works
Architecture
Key pieces
mimeTypesgetMimeTypebinary/octetroothandleRequestserverRequest flow
Running it
Prerequisites
1. Configure the root directory
Open
WebServer.fsxand update therootvalue to point to the folder containing your static files:2. Start the server
The process will block — that's the server running. Open your browser and navigate to:
3. Checking if the server is running
PowerShell / Windows:
Bash / Unix (Linux & macOS):
netstat -ano | grep 8090Both commands show any active TCP connections or listeners on port 8090. If the server is up, you will see a line with
LISTENING(Windows) orLISTEN(Unix) and the process ID.Serving an HTML file
Place any
.html,.htm,.txt,.jpg,.png, or.giffile in the root directory you configured. For example:wwwroot/hello.htmlThen browse to:
The root redirect (
/) always points toiisstart.htm— rename your default page to that, or change the redirect inhandleRequest:Testing with curl
Supported MIME types
.html,.htmtext/html.txttext/plain.gifimage/gif.jpgimage/jpeg.pngimage/pngbinary/octetLimitations
GETis supported;POST,HEAD, etc. return 404127.0.0.1(localhost)These are intentional — the goal is clarity, not production use.
Credits
The design of this web server is based on an example from Expert F# by Don Syme, Adam Granicz, and Antonio Cisternino. All credit for the original architecture goes to those authors.
Great for learning
asyncworkflows anduseresource management