---
title: 'Legacy Protocols'
source: 'https://academia.sh/en/courses/api-design/legacy-protocols'
course: 'Web API Design'
language: en
updated: '2026-08-19T05:19:28+00:00'
license: 'CC BY-SA 4.0'
---

# Legacy Protocols

The design and legacy of envelope-based protocols: separating header from body, carrying the operation name inside the envelope, reporting errors in the envelope instead of the transport layer, how the envelope's cost changes with record count, and which of today's interface decisions were inherited from here.

The previous lesson built an interface that works over a single endpoint, with a
selection document carried in the body. This mechanism is not new. There is a family of
protocols that built the same idea — single address, single method, naming the operation
in the body — much earlier, and part of today's interface decisions are inherited
directly from there.

This family's common building block is the **envelope**: a container that wraps the
entire message and holds a **header** and a **body** inside it. **SOAP** is the
best-known example of this structure and is built on XML; its `Envelope`, `Header`,
`Body`, and `Fault` elements are exactly what this lesson's `Envelope`, `Header`, `Body`,
and `Fault` elements are built to match.

## The Idea of the Envelope

The problem the envelope solves is this: carrying a message's own metadata alongside it,
independent of the protocol carrying the message. HTTP headers already do this job, but
the envelope design is not tied to HTTP — the same envelope can travel over a message
queue or a mail message too. Its cost and its gain sit together.

The envelope has three structural decisions:

- **The header carries cross-cutting concerns:** the trace id, credentials, the operation
  name. Intermediate layers can read this part and decide without ever parsing the body.
- **The body carries the payload:** data specific to the operation. The payload's shape
  changes from operation to operation.
- **The error also goes into the body:** failure is reported not by the transport layer's
  status code but by a fault element inside the body.

## A Small Implementation

The following file contains the functions that build and read the envelope. The XML
parser converts nested elements into an object; it does not support attributes or
namespaces, because what the lesson needs is the structure itself.

```js
// envelope.mjs — a small XML parser and envelope builders.
// Envelope/Header/Body/Fault elements are this lesson's counterpart to SOAP's Envelope/Header/Body/Fault.
const escape = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const unescape = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");

export const writeXml = (name, content) =>
  typeof content === "object" && content !== null
    ? `<${name}>${Object.entries(content).map(([a, d]) => writeXml(a, d)).join("")}</${name}>`
    : `<${name}>${escape(content)}</${name}>`;

// Parser: turns nested elements into an object. Siblings with the same name are collected into an array.
export const readXml = (text) => {
  const element = /<(\w+)>([\s\S]*?)<\/\1>/g;
  const output = {};
  let e, found = false;
  while ((e = element.exec(text))) {
    found = true;
    const value = /<\w+>/.test(e[2]) ? readXml(e[2]) : unescape(e[2]);
    if (e[1] in output) output[e[1]] = [].concat(output[e[1]], value);
    else output[e[1]] = value;
  }
  return found ? output : unescape(text);
};

export const wrap = (operation, trace, body) =>
  `<Envelope>${writeXml("Header", { Operation: operation, Trace: trace })}` +
  `<Body>${Object.entries(body).map(([a, d]) => writeXml(a, d)).join("")}</Body></Envelope>`;

export const faultEnvelope = (code, reason, detail) =>
  `<Envelope><Header><Operation>Fault</Operation></Header><Body>` +
  writeXml("Fault", { Code: code, Reason: reason, Detail: detail }) + `</Body></Envelope>`;

// The one place the intermediary reads: the header can be taken without parsing the body at all.
export const extractHeader = (envelope) => readXml(/<Header>[\s\S]*?<\/Header>/.exec(envelope)[0]).Header;
```

```js
// server.mjs — single endpoint, single method: the operation name is in the envelope's header.
import { createServer } from "node:http";
import { readXml, wrap, faultEnvelope, extractHeader } from "./envelope.mjs";

const BOOKS = {
  "978-0262033848": { isbn: "978-0262033848", title: "Introduction to Algorithms", author: "Cormen", pages: 1312 },
  "978-0201896831": { isbn: "978-0201896831", title: "The Art of Computer Programming", author: "Knuth", pages: 650 },
};

createServer(async (req, res) => {
  res.sendDate = false;
  let raw = "";
  for await (const p of req) raw += p;
  const header = extractHeader(raw);                  // routing: only the header is read
  const body = readXml(raw).Envelope.Body;
  let output;
  if (header.Operation === "GetBook") {
    const book = BOOKS[body.BookRequest.isbn];
    output = book ? wrap("GetBookResponse", header.Trace, { Book: book })
                  : faultEnvelope("Client", "book not found", `isbn: ${body.BookRequest.isbn}`);
  } else output = faultEnvelope("Client", "unknown operation", header.Operation);
  res.setHeader("Content-Type", "text/xml; charset=utf-8");
  res.setHeader("Content-Length", Buffer.byteLength(output));
  // Both faults and successful requests return with the same status code: the error is in the envelope, not the transport layer.
  res.writeHead(200).end(output);
}).listen(8481, "127.0.0.1", () => console.log("envelope endpoint 127.0.0.1:8481/service"));
```

## Measurement

```js
// measure.mjs — measures three properties of the envelope-based protocol
import { wrap, readXml, extractHeader } from "./envelope.mjs";

const call = async (operation, trace, body) => {
  const request = wrap(operation, trace, body);
  const y = await fetch("http://127.0.0.1:8481/service",
    { method: "POST", headers: { "Content-Type": "text/xml" }, body: request });
  const text = await y.text();
  return { status: y.status, sent: Buffer.byteLength(request), received: Buffer.byteLength(text),
           text, parsed: readXml(text).Envelope.Body };
};

console.log("-- 1) single endpoint, operation name in the envelope --");
const good = await call("GetBook", "trace-1", { BookRequest: { isbn: "978-0262033848" } });
console.log(`  request  : ${wrap("GetBook", "trace-1", { BookRequest: { isbn: "978-0262033848" } })}`);
console.log(`  response : ${good.text}`);
console.log(`  HTTP ${good.status}  sent=${good.sent} B  received=${good.received} B`);

console.log("\n-- 2) envelope cost: the same data in JSON and envelope form --");
const RECORD = { isbn: "978-0262033848", title: "Introduction to Algorithms", author: "Cormen", pages: 1312 };
for (const quantity of [1, 5, 20]) {
  const list = Array.from({ length: quantity }, () => RECORD);
  const json = Buffer.byteLength(JSON.stringify({ Book: list }));
  const envelope = Buffer.byteLength(wrap("GetBookResponse", "trace-1", { Book: list }));
  console.log(`  ${String(quantity).padStart(2)} record  json=${String(json).padStart(4)} B  ` +
    `envelope=${String(envelope).padStart(4)} B  difference=%${Math.round(100 * (envelope / json - 1))}`);
}

console.log("\n-- 3) the error is in the envelope, not the transport layer --");
const bad = await call("GetBook", "trace-2", { BookRequest: { isbn: "978-0000000000" } });
console.log(`  response : ${bad.text}`);
console.log(`  a client that looks at the status code -> HTTP ${bad.status} : "request successful"`);
console.log(`  a client that parses the body           -> ${JSON.stringify(bad.parsed.Fault)}`);

console.log("\n-- 4) an intermediary reads the header without parsing the body --");
for (const msg of [good.text, bad.text]) {
  const bodyLength = /<Body>[\s\S]*<\/Body>/.exec(msg)[0].length;
  console.log(`  header=${JSON.stringify(extractHeader(msg))}  (body ${bodyLength} B, not parsed)`);
}
```

```bash
node server.mjs & p=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null -X POST \
  -d '<Envelope><Header><Operation>x</Operation></Header><Body></Body></Envelope>' http://127.0.0.1:8481/service
node measure.mjs
kill $p
```

```
envelope endpoint 127.0.0.1:8481/service
-- 1) single endpoint, operation name in the envelope --
  request  : <Envelope><Header><Operation>GetBook</Operation><Trace>trace-1</Trace></Header><Body><BookRequest><isbn>978-0262033848</isbn></BookRequest></Body></Envelope>
  response : <Envelope><Header><Operation>GetBookResponse</Operation><Trace>trace-1</Trace></Header><Body><Book><isbn>978-0262033848</isbn><title>Introduction to Algorithms</title><author>Cormen</author><pages>1312</pages></Book></Body></Envelope>
  HTTP 200  sent=157 B  received=234 B

-- 2) envelope cost: the same data in JSON and envelope form --
   1 record  json= 104 B  envelope= 241 B  difference=%132
   5 record  json= 480 B  envelope= 709 B  difference=%48
  20 record  json=1890 B  envelope=2484 B  difference=%31

-- 3) the error is in the envelope, not the transport layer --
  response : <Envelope><Header><Operation>Fault</Operation></Header><Body><Fault><Code>Client</Code><Reason>book not found</Reason><Detail>isbn: 978-0000000000</Detail></Fault></Body></Envelope>
  a client that looks at the status code -> HTTP 200 : "request successful"
  a client that parses the body           -> {"Code":"Client","Reason":"book not found","Detail":"isbn: 978-0000000000"}

-- 4) an intermediary reads the header without parsing the body --
  header={"Operation":"GetBookResponse","Trace":"trace-1"}  (body 136 B, not parsed)
  header={"Operation":"Fault"}  (body 115 B, not parsed)
```

## What the Measurement Says

**The envelope's cost is fixed, the payload's cost is variable.** For a single record, the
envelope is 132% larger than the same data's JSON form; at twenty records, this difference
drops to 31%. The envelope itself — the header section, the wrapping elements — is a
fixed cost independent of record count; the per-record overhead comes from the closing
tags and repeats with every record. The envelope is expensive for small, frequent calls;
the ratio drops for large payloads.

**The cost of reporting the error in the envelope lands on the consumer.** When a
nonexistent book was requested, the server returned `200`. A client that looks at the
status code counts the request as successful; seeing the error requires parsing the body.
This is the source of the error contract splitting in two, mentioned in the fourth lesson,
and it comes back in exactly the same shape in GraphQL's partial success response.

**The header can be read independently of the body.** The last part of the measurement
shows that a body over a hundred bytes can have its header taken without being parsed at
all. This means an intermediate layer — a router, an authorization layer, a tracing layer
— can do its job without understanding the payload. The **trace id** the header carries
links the same request's records across different components together.

## The Legacy Left Behind

The aspects of envelope-based design carried forward to today are a direct continuation of
the properties seen in the measurement.

**A machine-readable contract.** This family's interfaces publish which operations exist
and each operation's input–output types in a separate definition file. Two habits remain
from this: the contract being an executable file separate from documentation, and the
client side being generated from that file. The same idea shows up as the schema file in
the fourth lesson, and later in this course as OpenAPI- and JSON-Schema-based definitions.

**A structured error.** The triple inside the fault element — code, reason, detail — is
the ancestor of today's standard error bodies: a code the machine will read, an
explanation the human will read, and a context-specific extra. This structure is detailed
in the Error Response Format lesson.

**Separating cross-cutting concerns.** Keeping identity, trace, and routing information
apart from the payload has been preserved; in HTTP-based interfaces, this separation lives
in HTTP headers instead of the envelope's header.

**Carrying the operation name in the body.** The pattern of a single endpoint with the
operation name in the body persists in both remote-procedure-call-style HTTP interfaces
and in GraphQL.

What was not inherited is just as clear. The desire to be transport-independent renders
HTTP's own semantics — methods, status codes, conditional requests, cache directives —
unused. The cacheability gain measured in the third lesson cannot be obtained in a design
where every request is `POST`. Together with the text format's overhead of detail, this is
the real reason that opened the way for designs that do the same job with fewer bytes and
fewer layers.

## Summary

- Envelope-based protocols work with a container that wraps the message as a header and a
  body; the operation name is carried not in the address but in the envelope's header, and
  the transport layer is treated as replaceable.
- In the measurement, the envelope's byte cost came out to 132% for a single record and
  31% for twenty records: the envelope itself is a fixed cost, and the closing tags are a
  cost that repeats per record.
- Because the error is reported inside the envelope, the server returns `200` even on a
  failed operation; a client that looks at the status code cannot see the error and is
  forced to parse the body.
- Because the header can be read independently of the body, intermediate layers can do
  routing, authorization, and tracing without ever parsing the payload.
- The inherited legacy is four items: the machine-readable contract file, the structured
  error shape, separating cross-cutting concerns from the payload, and a named operation
  over a single endpoint.

## Next Step

The shared assumption of the five styles so far is the same: the client initiates
communication, the server responds, the exchange ends. Some of the library service's jobs
do not fit this pattern. A book returning to the shelf is an event that happens on the
server, and nobody asks the waiting client about it. A count job runs for minutes, and
keeping the request open is not an option. The next lesson builds communication forms
outside the request–response pattern and compares two of them by measurement: how many
connections and how much delay does an event stream versus long polling take to deliver
the same notification?
