An HTTP/JSON API server written entirely in COBOL. socat holds the socket; COBOL parses requests, routes, and emits JSON.
Find a file
2026-06-30 21:26:47 -04:00
scripts fix: read the POST body by Content-Length, not a blocking line read 2026-06-30 18:32:47 -04:00
src fix: match Content-Length case-insensitively (RFC 7230) 2026-06-30 21:08:58 -04:00
test fix: read the POST body by Content-Length, not a blocking line read 2026-06-30 18:32:47 -04:00
.gitignore chore: keep dev-process docs local (gitignore session notes + superpowers) 2026-06-30 21:26:47 -04:00
AGENTS.md chore: scaffold cobweb (workspace standard) 2026-06-30 14:31:56 -04:00
CHANGELOG.md docs: changelog + README + session notes for v1.0.1 2026-06-30 21:12:14 -04:00
CLAUDE.md chore: scaffold cobweb (workspace standard) 2026-06-30 14:31:56 -04:00
Containerfile chore: drop Containerfile --system, document run.sh ,pipes, trim long comments 2026-06-30 18:16:42 -04:00
GEMINI.md chore: scaffold cobweb (workspace standard) 2026-06-30 14:31:56 -04:00
LICENSE chore: scaffold cobweb (workspace standard) 2026-06-30 14:31:56 -04:00
Makefile feat: multi-stage Containerfile + make image 2026-06-30 16:58:40 -04:00
README.md docs: changelog + README + session notes for v1.0.1 2026-06-30 21:12:14 -04:00

cobweb

License: MIT Language: GnuCOBOL 3.2

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.cobbin/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/health returns nothing, localhost may be resolving to ::1 (IPv6) while the container is bound to 0.0.0.0 (IPv4). Use http://127.0.0.1:8080/health instead.

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 text values: 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%3Db yields a=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>.sh unit test for any new endpoint.
  • Run make test before 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.