odal passport suspend is documented as "Suspend a published passport (serves 410 Gone)". On a freshly booted node, a suspended passport is served two different and both-wrong ways depending on which public route the consumer arrives by.
Reproduced end to end: fresh stack, 5 textile passports imported and published via the CLI, then one suspended with odal passport suspend.
1. /dpp/{id} — the status line and the problem body disagree
$ curl -s -D- http://localhost:8103/dpp/01a0256c-c082-7641-8b56-ceb3b1526904
HTTP/1.1 410 Gone
content-type: application/json
{"detail":"DPP not found","status":404,"title":"Not Found",
"type":"https://problems.odal-node.io/not-found","requestId":"…"}
The status line is 410 Gone, which is right. The RFC 7807 body inside it claims "status": 404 and "title": "Not Found". RFC 7807 §3.1 says the status member is "the HTTP status code generated by the origin server for this occurrence of the problem" — it is meant to mirror the status line, and here it contradicts it.
detail is also wrong on its own terms: "DPP not found" is a claim that the passport does not exist. A suspended passport does exist and has been withdrawn, which is the entire distinction 410 carries.
Any client that trusts the body over the status line — a reasonable thing to do, since the body is the structured half — concludes the passport never existed.
2. The GS1 Digital Link route loses the withdrawal signal entirely
That is the /dpp/{id} route. The route a consumer actually reaches by scanning the QR code is the GS1 Digital Link path, and it does not return 410 at all:
$ curl -s -o /dev/null -w '%{http_code}' \
'http://localhost:8103/01/03801234567898/10/BATCH-SS26-001/21/76418b56ceb3b1526904'
404
{"error":"NOT_FOUND","message":"No published DPP for this GTIN","requestId":"…"}
For an active passport the same route answers 307 and redirects to /dpp/{id}. For a suspended one it 404s before redirecting, so the 410 on /dpp/{id} is never reached.
This is the more consequential half. Serving 410 for a withdrawn passport is a recall signal, and the audience for that signal is whoever scans the code on the product. They take the GS1 path, and on that path a recalled product is indistinguishable from a GTIN that was never registered.
Note the two routes also use different error shapes: /dpp/{id} returns RFC 7807 (type/title/detail/status), the GS1 route returns {"error","message","requestId"}.
Suggested direction
- Make the problem body on
/dpp/{id} agree with its own status line, and say the passport was withdrawn rather than not found.
- Have the GS1 Digital Link lookup distinguish "no such GTIN" from "this GTIN resolves to a withdrawn passport", and serve 410 for the latter — either directly or by redirecting to
/dpp/{id} and letting that route answer.
- Consider whether the two public routes should share one error shape.
Found during a from-scratch CLI walkthrough of a freshly booted stack.
odal passport suspendis documented as "Suspend a published passport (serves 410 Gone)". On a freshly booted node, a suspended passport is served two different and both-wrong ways depending on which public route the consumer arrives by.Reproduced end to end: fresh stack, 5 textile passports imported and published via the CLI, then one suspended with
odal passport suspend.1.
/dpp/{id}— the status line and the problem body disagreeThe status line is
410 Gone, which is right. The RFC 7807 body inside it claims"status": 404and"title": "Not Found". RFC 7807 §3.1 says thestatusmember is "the HTTP status code generated by the origin server for this occurrence of the problem" — it is meant to mirror the status line, and here it contradicts it.detailis also wrong on its own terms: "DPP not found" is a claim that the passport does not exist. A suspended passport does exist and has been withdrawn, which is the entire distinction 410 carries.Any client that trusts the body over the status line — a reasonable thing to do, since the body is the structured half — concludes the passport never existed.
2. The GS1 Digital Link route loses the withdrawal signal entirely
That is the
/dpp/{id}route. The route a consumer actually reaches by scanning the QR code is the GS1 Digital Link path, and it does not return 410 at all:For an active passport the same route answers
307and redirects to/dpp/{id}. For a suspended one it 404s before redirecting, so the 410 on/dpp/{id}is never reached.This is the more consequential half. Serving 410 for a withdrawn passport is a recall signal, and the audience for that signal is whoever scans the code on the product. They take the GS1 path, and on that path a recalled product is indistinguishable from a GTIN that was never registered.
Note the two routes also use different error shapes:
/dpp/{id}returns RFC 7807 (type/title/detail/status), the GS1 route returns{"error","message","requestId"}.Suggested direction
/dpp/{id}agree with its own status line, and say the passport was withdrawn rather than not found./dpp/{id}and letting that route answer.Found during a from-scratch CLI walkthrough of a freshly booted stack.