uclogserver
Log in

Errors, limits & CORS

Examples use the public sandbox tokens, so you can run them as they are.

Error format

Errors are RFC 9457 problem documents, except where the endpoint answers in plain text (see Plain-text errors):

HTTP/1.1 404 Not Found
Content-Type: application/problem+json
X-Request-Id: 7b370052-9487-42fd-813f-af5b68db84a6

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "firmware not found",
  "instance": "/firmware/00000000-0000-0000-0000-000000000000",
  "request_id": "7b370052-9487-42fd-813f-af5b68db84a6"
}

Some errors add fields, e.g. what_strings, missing and conflicts on firmware 422s, conflicts or errors on device imports, and id on firmware 409s.

Plain-text errors

The endpoints devices and build scripts call directly — firmware uploads (PUT /firmware, PUT /firmware/{hw}/{hwver}/{fw}), the update check (GET /firmware/{domain}/…) and PUT/GET /logdata/{name} — support compact plain-text answers. Unless the request asks for JSON, their errors are one line of text (see Response formats):

HTTP/1.1 409 Conflict
Content-Type: text/plain; charset=utf-8

409 Conflict: a log file with this name already exists with different content (log files cannot be replaced)

Send Accept: application/json to get problem documents from these endpoints too:

curl -si -X PUT https://fw.unitcircle.ca/firmware -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA" -H 'Accept: application/json' \
     --form-string 'firmware=@(#)1.6.0-10-g30d0049, demo, example-hw@2.0.0, 2025-12-05T15:25:18-05:00, AFI' \
     --form-string 'notes=# a different build claiming the same version' 
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{"detail":"firmware demo 1.8.0 for example-hw@2.0.0 (AFI) already exists with different content (firmware cannot be replaced; release a new version)","id":"01a0daca-299d-7acd-9e99-176507b3f170","instance":"/firmware","request_id":"76257c73-…","status":409,"title":"Conflict","type":"about:blank"}

Request ids

Every response carries X-Request-Id. Send your own (8–64 characters of letters, digits, ., _, -) to correlate with your logs, or the service generates a UUID. Quote it when asking for support; operators can trace it in the logs and the access log.

Status codes

Status Meaning here Typical causes
200 OK also an idempotent re-upload of identical bytes
201 Created new firmware, log file, device, token, member, customer
202 Accepted login code requested (always); backup/restore queued
204 No Content tag add/remove/delete, revocations, logout
302 Found log/firmware download → /dl/… → object store
400 Bad Request invalid name/hwid (1–64 printable characters, no spaces)/version/tag/date, malformed JSON, unknown JSON field, missing multipart part
401 Unauthorized no token, bad, revoked or expired token, wrong login code, expired session
403 Forbidden missing scope, missing CSRF token, not an admin, blocked IP, CORS preflight from a disallowed origin
404 Not Found unknown object, or a customer you can't access (?customer= for someone else; never 403, so customers can't be probed)
405 Method Not Allowed wrong method for the URL, e.g. DELETE on firmware, log files or devices (they are never deleted)
409 Conflict same name/identity with different bytes, re-upload of a withdrawn build, changing a withdrawn device, device key change (use ?force=true), import conflicts, last owner, duplicate customer/tag
410 Gone expired or invalid download link
412 Precondition Failed stale If-Match on firmware PATCH
413 Payload Too Large size limits below
422 Unprocessable Entity firmware what-string problems (missing/conflicting fields, no or several what strings, bad version)
429 Too Many Requests rate limit; see Retry-After
500 Internal Server Error a bug on our side; the detail includes the request id
503 Service Unavailable maintenance mode (writes only, Retry-After: 300); /status//readyz when dependencies are down

Size limits

What Limit
Firmware binary 16 MiB (operator-configurable)
Release notes 1 MiB, UTF-8
Log file 16 MiB (operator-configurable)
JSON request bodies 1 MiB
Device import 8 MiB, 10,000 rows
What string 512 bytes
HTTP/1.1 413 Request Entity Too Large
Content-Type: text/plain; charset=utf-8

413 Request Entity Too Large: logdata larger than 16777216 bytes

Rate limits

Token buckets, per client IP unless noted. Exceeding one gives 429 with Retry-After (seconds):

Class Applies to Sustained Burst
update check GET /firmware/{domain}/{hw}/{hwver}/{fw}/{fwver}[/{hwid}] 60 / min 30
downloads /dl/… 120 / min 60
login POST /auth/login (plus max. 5 codes per email per hour) 20 / hour 10
code verification POST /auth/verify (plus max. 5 guesses per code) 30 / hour 10
API token any request with a Bearer token, per token 600 / min 200
HTTP/1.1 429 Too Many Requests
Content-Type: text/plain; charset=utf-8
Retry-After: 1

429 Too Many Requests: rate limit exceeded; retry after 1 s

Repeated 429s and other abuse patterns raise security events for the operator, and the IP can be blocked (403 your address has been blocked; contact the service operator).

CORS

URLs Cross-origin policy
Update checks, /status, /healthz, /readyz, /health/history, /.well-known/jwks.json, /openapi.json any origin (Access-Control-Allow-Origin: *), no credentials
Customer API (/firmware…, /logdata…, /devices…, /tags…, /tokens…, /members…, /stats/…) only origins the operator allows globally, or that are registered by an active customer (cors_origins); the origin is echoed back; Bearer tokens only (no cookies). The token still decides whose data you reach.
/auth/…, /admin/…, console, /dl/… same-origin only (no CORS headers)

Allowed preflight methods: GET, HEAD, PUT, POST, PATCH, DELETE. Allowed headers: Authorization, Content-Type, If-Match, X-Request-Id. Exposed headers: X-Request-Id, Location, Retry-After, ETag. Preflights are cached for 10 minutes.

curl -si -X OPTIONS https://fw.unitcircle.ca/devices -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: GET'
HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, X-Request-Id
Access-Control-Allow-Methods: GET, HEAD, PUT, POST, PATCH, DELETE
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Expose-Headers: X-Request-Id, Location, Retry-After, ETag
Access-Control-Max-Age: 600
Vary: Origin
OPTIONS /devices
Origin: https://evil.example
Access-Control-Request-Method: GET

HTTP/1.1 403 Forbidden
{"detail":"origin not allowed", …}
OPTIONS /firmware/sandbox.example/example-hw/2.0.0/demo/0.0.0
Origin: https://any.example
Access-Control-Request-Method: GET

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, PUT, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, X-Request-Id
Access-Control-Max-Age: 600

To call the customer API from your own web app, ask your account administrator to add your app's origin (e.g. https://portal.example.com) to your customer's CORS origins.

Unknown URLs

GET /no/such/url
HTTP/1.1 404 Not Found

{"detail":"no such URL; see https://fw.unitcircle.ca/docs", …}

Security headers

All responses include X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, X-Frame-Options: DENY, and Strict-Transport-Security on HTTPS. Downloaded files are always served as attachments.