| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Containerfile | ||
| GEMINI.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
cobweb
An HTTP/JSON API server written entirely in COBOL. socat holds the socket; COBOL parses requests, routes, and emits JSON.
Table of Contents
Background
COBOL is sixty-plus years old, runs a non-trivial fraction of the world's financial transactions, and is usually described as a language nobody writes anymore. This project tests that claim — by writing an HTTP/JSON API server in it.
The architecture boundary is the point: COBOL owns the entire HTTP protocol
above the wire. Request parsing, header scanning, URL decoding, routing,
JSON serialisation, status codes, CRLF framing — all COBOL. socat contributes
exactly one thing: it accepts TCP connections and forks one short-lived COBOL
process per request, with the raw socket data piped to the process on stdin and
the process's stdout piped back to the client. That boundary also makes the
whole HTTP layer unit-testable with zero sockets — every test/test_*.sh feeds
a raw request to ./bin/cobweb-handle via stdin and checks the stdout.
Cobweb is a portfolio showpiece, not a production framework. It has bounded
buffers (8 KiB request / body), Connection: close on every response, and
one process per request. None of those are apologised for — they are correct
tradeoffs for a program whose entire purpose is to demonstrate that COBOL can
speak HTTP at all.
Architecture
curl / browser
|
| TCP
v
socat TCP-LISTEN:8080,reuseaddr,fork
|
| stdin (pipe) stdout (pipe)
v |
cobweb-handle (COBOL) |
READ-REQUEST |
PARSE-REQUEST |
ROUTE |
SEND-RESPONSE ────────────────────>─┘
|
| (process exits; socat closes connection)
socat is the inetd-style shim: it never touches the HTTP bytes. Every byte
of the HTTP request is parsed, and every byte of the HTTP response is composed,
in COBOL.
Install
Arch Linux (AUR)
GnuCOBOL is available in the AUR — use paru (or your AUR helper of choice):
paru -S gnucobol socat
make build
Debian / Ubuntu
sudo apt-get install gnucobol socat
make build
Container (no host COBOL needed)
If you just want to run cobweb without installing a COBOL compiler, the container path is self-contained — see Container (podman) below.
Usage
Local (native)
Start the listener on port 8080:
make run
This compiles src/cobweb-handle.cob → bin/cobweb-handle (if needed), then
launches socat TCP-LISTEN:8080,reuseaddr,fork EXEC:./bin/cobweb-handle,pipes.
Each incoming connection forks one COBOL process; the process exits after
sending its response.
Try it:
# Health check
curl http://localhost:8080/health
{"status":"ok","server":"GnuCOBOL"}
# A COBOL-flavoured fortune
curl http://localhost:8080/api/fortune
{"fortune":"DIVIDE worry BY zero GIVING calm REMAINDER wisdom."}
# Current time (ISO-8601 + Unix epoch, computed in COBOL)
curl http://localhost:8080/api/time
{"utc":"2026-06-30T17:24:15","epoch":1782840255}
# Uppercase transform
curl 'http://localhost:8080/api/upcase?text=hello%20cobol'
{"input":"hello cobol","upcase":"HELLO COBOL"}
# ROT-13 (because of course)
curl 'http://localhost:8080/api/rot13?text=hello'
{"input":"hello","rot13":"uryyb"}
# Echo the request body back as JSON
curl -X POST \
-H "Content-Type: application/json" \
--data '{"msg":"hello from COBOL"}' \
http://localhost:8080/api/echo
{"youSent":"{\"msg\":\"hello from COBOL\"}","bytes":26}
# HTML landing page — a styled, interactive console: every endpoint is a
# clickable card that runs in-browser via fetch and renders the reply inline
curl -si http://localhost:8080/
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 4778
Connection: close
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>cobweb</title>
<style>/* dark green-phosphor terminal theme */ ...</style>
</head>
<body>
<div class="wrap">
<h1>cobweb</h1>
...clickable endpoint cards, each with a hidden response panel...
<script>/* click/Enter -> fetch(endpoint) -> render JSON inline */ ...</script>
</body>
</html>
# 404 — unknown path
curl -si http://localhost:8080/nope
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 36
Connection: close
{"error":"not found","path":"/nope"}
Container (podman)
The multi-stage Containerfile compiles the COBOL in a Debian builder stage and
produces a minimal runtime image. No COBOL compiler on the host required.
# Build the image
make image
# Run (detached, port 8080)
podman run --rm -d -p 8080:8080 --name cobweb cobweb
# Confirm it is up
curl http://127.0.0.1:8080/health
{"status":"ok","server":"GnuCOBOL"}
# Check the built-in health check
podman healthcheck run cobweb
# Tear down
podman rm -f cobweb
Note: If
curl http://localhost:8080/healthreturns nothing,localhostmay be resolving to::1(IPv6) while the container is bound to0.0.0.0(IPv4). Usehttp://127.0.0.1:8080/healthinstead.
API
| Method | Path | Description | Example |
|---|---|---|---|
GET |
/ |
HTML landing page listing all endpoints | curl http://localhost:8080/ |
GET |
/health |
Liveness probe | curl http://localhost:8080/health |
GET |
/api/fortune |
Random COBOL-themed fortune | curl http://localhost:8080/api/fortune |
GET |
/api/time |
Current time as ISO-8601 string + Unix epoch | curl http://localhost:8080/api/time |
GET |
/api/upcase?text= |
Uppercase the text query parameter (URL-decoded) |
curl 'http://localhost:8080/api/upcase?text=hello' |
GET |
/api/rot13?text= |
ROT-13 the text query parameter (URL-decoded) |
curl 'http://localhost:8080/api/rot13?text=hello' |
POST |
/api/echo |
Echo the request body; returns {"youSent":…,"bytes":N} |
see Usage |
Encoding
textvalues: the value is taken from the first query parameter regardless of its name, and an unencoded=ends it — everything after the first=is dropped. Encode a literal=as%3D(e.g.?text=a%3Dbyieldsa=b).
Error responses follow the same JSON envelope:
| Status | Condition |
|---|---|
400 Bad Request |
Malformed or empty request line |
404 Not Found |
Unknown path |
405 Method Not Allowed |
Wrong method for a known path |
413 Payload Too Large |
Request body exceeds 8 KiB |
Operational Notes
socat EXEC:…,pipes — do not remove the ,pipes flag
socat's default EXEC: transport attaches the child's stdin/stdout to a
Unix-domain socketpair. GnuCOBOL opens /dev/stdin as a SEQUENTIAL
file using standard C FILE* I/O, which fails with ENXIO on a socket fd.
The ,pipes option instructs socat to create real pipe fds instead, which
the COBOL child can open and read normally.
Removing ,pipes causes every request to fail with a file-open error.
POST body is read by Content-Length
The body is read as exactly the number of bytes named in the Content-Length
header, then the read stops — it never waits for a trailing newline or for the
client to close the connection. The header name is matched case-insensitively
(RFC 7230), so a lowercase content-length: from HTTP/2 clients, browser
fetch, and node/undici is read the same as curl's capitalized form. curl's
plain --data works directly; no --data-binary $'…\n' newline workaround is
needed. A body larger than the 8 KiB buffer returns 413 Payload Too Large.
podman build --format docker
Podman's default image format is OCI. The OCI image spec does not include a
HEALTHCHECK instruction, so podman silently drops the HEALTHCHECK from the
Containerfile when building in OCI mode. Building with --format docker
(which make image does) preserves the health check so that
podman healthcheck run cobweb works as expected.
Contributing
Primary remote: codeberg.org/alan090/cobweb
Mirror: github.com/alan09086/cobweb
Pull requests welcome on Codeberg. A few house rules:
- Keep COBOL in
src/cobweb-handle.cob; all HTTP logic stays there. - Add a
test/test_<feature>.shunit test for any new endpoint. - Run
make testbefore opening a PR — unit suite + live smoke test must pass. - Commits are attributed
Alan <alan.c.gaudet@gmail.com>. No AI co-author trailer in commits.
License
MIT — Copyright (c) 2026 Alan Gaudet — see LICENSE.