The HTTP API¶
The surface¶
| Method | Path | Handler | Purpose |
|---|---|---|---|
GET |
/schemas |
Read | Every published identifier |
GET |
/schemas/{name}/versions |
Read | The versions of one name |
GET |
/schemas/{name}/{version} |
Read | The schema, as its own bytes |
PUT |
/schemas/{name}/{version} |
Write | Publish |
Under serve.Mount the read
handler is at the root and the write handler is under /publish, so the publish path is
PUT /publish/schemas/{name}/{version}.
Headers¶
| Header | Direction | Meaning |
|---|---|---|
X-Schema-Language |
Both | jsonschema or protobuf |
X-Schema-Root |
Both | The root message of a protobuf descriptor set. Absent for JSON Schema |
Reading a schema¶
$ curl -i https://schema.internal.example/schemas/orders.created/3
HTTP/1.1 200 OK
Content-Type: application/schema+json
X-Schema-Language: jsonschema
{"type":"object","required":["order"]}
Schemas are served as their own bytes, not wrapped in an envelope, so a JSON Schema is readable
in a browser and a descriptor set is usable by anything that reads one. A descriptor set is served
as application/octet-stream rather than mislabelled as JSON.
Listing¶
$ curl https://schema.internal.example/schemas
{"schemas":["urn:phpboyscout:schema:orders.created:1","urn:phpboyscout:schema:orders.created:3"]}
$ curl https://schema.internal.example/schemas/orders.created/versions
{"versions":[1,3]}
$ curl https://schema.internal.example/schemas/nothing.here/versions
{"versions":[]}
Both are ordered — by name then version — so two calls against an unchanged registry agree. A name
with no versions is [], never null: JSON null forces every client to special-case a value that
means the same thing as empty.
Publishing¶
$ curl -X PUT https://schema.internal.example/publish/schemas/orders.created/3 \
-H 'X-Schema-Language: jsonschema' \
--data-binary @orders.created.v3.json
?compatibility= accepts only none, and defaults to it. Bodies are capped at 1 MiB and an
oversize one is 413; an unbounded read on a write path is a denial of service with extra steps.
Status codes¶
Each one is a statement about whose problem it is.
| Code | When |
|---|---|
200 |
Read succeeded |
201 |
Published, including a republication with identical bytes |
400 |
The version is not canonical, the name breaks the URN grammar, the language is unknown, the body is empty, or a compatibility mode other than none was asked for |
404 |
Nothing is published under that identifier |
409 |
That identifier is already published with different bytes |
413 |
The body exceeds 1 MiB |
500 |
The registry's own storage failed |
Versions must be canonical¶
/schemas/orders.created/3 resolves; /01, /007, /+1 and /1.0 are all 400.
The path segment goes into the URN grammar as it arrived rather than through strconv.Atoi, which
accepts 007 and +1. Laundering it would give one schema unlimited URLs: a cache-key explosion on
the read path, and on the write path a way past any proxy rule keyed on the URL — a policy denying
PUT /publish/schemas/payments.captured/1 would be evaded with /01.
The name is validated on every endpoint that takes one, including /versions. That one is the
unauthenticated surface and Store is a seam somebody will back with a filesystem.
Nothing the registry means returns a 500, so an alert on the 500 rate means something.
Why not Confluent's API¶
Confluent's Schema Registry is built on a subject/version model:
GET /subjects
GET /subjects/{subject}/versions
POST /compatibility/subjects/{subject}/versions/{version}
with a registry-assigned integer id and an application/vnd.schemaregistry.v1+json content type.
We match none of it. Their API is organised around a mutable subject whose versions accumulate and whose ids the registry assigns — the direct opposite of a permanent identifier chosen by the publisher and refused if it changes. Adopting the endpoints would inherit a compatibility model, an id-assignment scheme and a subject concept we deliberately replaced, and buy no interoperability in return: nothing speaking their protocol works against ours once the addressing differs.
One idea is taken, because it is right. Compatibility is an operation a caller asks for
explicitly, rather than an implicit property of publishing. That is what the compatibility
parameter is, and it is why it exists before anything checks it.