From ce92382cead07adf9d38910656178bb8bcd5b1eb Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 15 Sep 2026 12:59:04 -0500 Subject: [PATCH 1/4] Forward RERUM Pagination-* headers from /query on success and error paths --- routes/query.js | 8 +++ test/TESTING.md | 11 ++++ test/routes/query.test.js | 107 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/routes/query.js b/routes/query.js index 9f84fbf..0b767d6 100644 --- a/routes/query.js +++ b/routes/query.js @@ -3,6 +3,9 @@ import { httpError, verifyJsonContentType } from "../rest.js" import { fetchRerum } from "../rerum.js" const router = express.Router() +// RERUM reports the applied and maximum limit and skip in these headers. Forward them so clients can tell a truncated page from a final one. +const PAGINATION_HEADERS = ["Pagination-Limit", "Pagination-Skip", "Pagination-Limit-Max", "Pagination-Skip-Max"] + /* POST a query to the thing. */ router.post('/', verifyJsonContentType, async (req, res, next) => { const lim = req.query.limit ?? 10 @@ -36,6 +39,11 @@ router.post('/', verifyJsonContentType, async (req, res, next) => { const queryURL = `${process.env.RERUM_API_ADDR}query?limit=${lim}&skip=${skip}` const rerumResponse = await fetchRerum(queryURL, queryOptions) .then(async (resp) => { + // Set before branching so the headers survive on the 502 error path as well + for (const header of PAGINATION_HEADERS) { + const value = resp.headers.get(header) + if (value !== null) res.set(header, value) + } if (resp.ok) return resp.json() // The response from RERUM indicates a failure, likely with a specific code and textual body let rerumErrorMessage diff --git a/test/TESTING.md b/test/TESTING.md index 6763f9f..cfeb281 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -86,6 +86,17 @@ This prevents accidental changes to: - ✅ Body field name (`isOverwritten` vs other names) - ✅ Precedence between header and body sources +### Pagination Header Forwarding + +RERUM reports the applied and maximum `limit` and `skip` on `/query` responses in the `Pagination-Limit`, `Pagination-Skip`, `Pagination-Limit-Max`, and `Pagination-Skip-Max` headers. The query route forwards them to the client. Tests in query.test.js validate: + +1. **Success path** — every `Pagination-*` header RERUM returns is on the 200 +2. **Error path** — the `Pagination-*` headers RERUM returns on a failure are on the 502 +3. **Absence handling** — no `Pagination-*` headers are added when RERUM sends none, or when RERUM is never reached +4. **No over-forwarding** — unrelated RERUM response headers are not copied + +Fetch mocks in query.test.js must include `headers: new Headers()` because the route reads `resp.headers`. + ### Future Improvements Once RERUM provides a reliable test harness, TinyNode will: diff --git a/test/routes/query.test.js b/test/routes/query.test.js index 6b45b36..190d5c3 100644 --- a/test/routes/query.test.js +++ b/test/routes/query.test.js @@ -15,6 +15,7 @@ routeTester.use(messenger) const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` const originalFetch = global.fetch +const paginationHeaders = ["pagination-limit", "pagination-skip", "pagination-limit-max", "pagination-skip-max"] let lastFetchUrl, lastFetchOptions beforeEach(() => { @@ -24,6 +25,7 @@ beforeEach(() => { lastFetchUrl = url lastFetchOptions = opts return { + headers: new Headers(), json: async () => ([{ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }]), ok: true, text: async () => "Descriptive Error Here" @@ -118,6 +120,7 @@ describe("Check that incorrect TinyNode query route usage results in expected RE describe("Query upstream and network failure behavior. __rest __core", () => { it("Preserves upstream text error message when query returns non-ok.", async () => { global.fetch = async () => ({ + headers: new Headers(), ok: false, status: 503, text: async () => "Upstream query failure" @@ -134,6 +137,7 @@ describe("Query upstream and network failure behavior. __rest __core", () => { it("Falls back to generic RERUM error text when upstream .text() throws.", async () => { global.fetch = async () => ({ + headers: new Headers(), ok: false, status: 500, text: async () => { @@ -162,6 +166,109 @@ describe("Query upstream and network failure behavior. __rest __core", () => { assert.equal(response.statusCode, 502) assert.match(response.text, /A RERUM error occurred/) + for (const header of paginationHeaders) { + assert.equal(response.headers[header], undefined, `${header} should not be set when RERUM was not reached`) + } + }) +}) + +describe("Query pagination header forwarding. __rest __core", () => { + it("Forwards every Pagination-* header RERUM returns on a successful query.", async () => { + global.fetch = async () => ({ + headers: new Headers({ + "Pagination-Limit": "500", + "Pagination-Skip": "0", + "Pagination-Limit-Max": "500", + "Pagination-Skip-Max": "100000" + }), + json: async () => ([{ "@id": rerumUri, test: "item" }]), + ok: true + }) + + const response = await request(routeTester) + .post("/query?limit=1000") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 200) + assert.equal(response.body[0].test, "item") + assert.equal(response.headers["pagination-limit"], "500") + assert.equal(response.headers["pagination-skip"], "0") + assert.equal(response.headers["pagination-limit-max"], "500") + assert.equal(response.headers["pagination-skip-max"], "100000") + }) + + it("Forwards the Pagination-* headers RERUM returns on an error response.", async () => { + global.fetch = async () => ({ + headers: new Headers({ + "Pagination-Limit-Max": "500", + "Pagination-Skip-Max": "100000" + }), + ok: false, + status: 400, + text: async () => "The 'skip' URL parameter of 100001 is beyond the maximum of 100000." + }) + + const response = await request(routeTester) + .post("/query?skip=100001") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /beyond the maximum of 100000/) + assert.equal(response.headers["pagination-limit-max"], "500") + assert.equal(response.headers["pagination-skip-max"], "100000") + assert.equal(response.headers["pagination-limit"], undefined) + assert.equal(response.headers["pagination-skip"], undefined) + }) + + it("Adds no Pagination-* headers when RERUM sends none.", async () => { + let response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 200) + for (const header of paginationHeaders) { + assert.equal(response.headers[header], undefined, `${header} should not be set on a 200`) + } + + global.fetch = async () => ({ + headers: new Headers(), + ok: false, + status: 500, + text: async () => "Upstream query failure" + }) + + response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + for (const header of paginationHeaders) { + assert.equal(response.headers[header], undefined, `${header} should not be set on a 502`) + } + }) + + it("Does not forward unrelated RERUM response headers.", async () => { + global.fetch = async () => ({ + headers: new Headers({ + "Pagination-Limit": "10", + "X-Unrelated": "nope" + }), + json: async () => ([{ "@id": rerumUri, test: "item" }]), + ok: true + }) + + const response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 200) + assert.equal(response.headers["pagination-limit"], "10") + assert.equal(response.headers["x-unrelated"], undefined) }) }) From 135c50583b1b46c56268bba4e5c3b2c9901656fa Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 15 Sep 2026 13:06:05 -0500 Subject: [PATCH 2/4] Support new pagination headers --- routes/query.js | 2 +- test/routes/query.test.js | 49 --------------------------------------- 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/routes/query.js b/routes/query.js index 0b767d6..11a3097 100644 --- a/routes/query.js +++ b/routes/query.js @@ -3,7 +3,7 @@ import { httpError, verifyJsonContentType } from "../rest.js" import { fetchRerum } from "../rerum.js" const router = express.Router() -// RERUM reports the applied and maximum limit and skip in these headers. Forward them so clients can tell a truncated page from a final one. +// RERUM reports the applied and maximum limit and skip in these headers. const PAGINATION_HEADERS = ["Pagination-Limit", "Pagination-Skip", "Pagination-Limit-Max", "Pagination-Skip-Max"] /* POST a query to the thing. */ diff --git a/test/routes/query.test.js b/test/routes/query.test.js index 190d5c3..083ec03 100644 --- a/test/routes/query.test.js +++ b/test/routes/query.test.js @@ -221,55 +221,6 @@ describe("Query pagination header forwarding. __rest __core", () => { assert.equal(response.headers["pagination-limit"], undefined) assert.equal(response.headers["pagination-skip"], undefined) }) - - it("Adds no Pagination-* headers when RERUM sends none.", async () => { - let response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send({ test: "item" }) - - assert.equal(response.statusCode, 200) - for (const header of paginationHeaders) { - assert.equal(response.headers[header], undefined, `${header} should not be set on a 200`) - } - - global.fetch = async () => ({ - headers: new Headers(), - ok: false, - status: 500, - text: async () => "Upstream query failure" - }) - - response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send({ test: "item" }) - - assert.equal(response.statusCode, 502) - for (const header of paginationHeaders) { - assert.equal(response.headers[header], undefined, `${header} should not be set on a 502`) - } - }) - - it("Does not forward unrelated RERUM response headers.", async () => { - global.fetch = async () => ({ - headers: new Headers({ - "Pagination-Limit": "10", - "X-Unrelated": "nope" - }), - json: async () => ([{ "@id": rerumUri, test: "item" }]), - ok: true - }) - - const response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send({ test: "item" }) - - assert.equal(response.statusCode, 200) - assert.equal(response.headers["pagination-limit"], "10") - assert.equal(response.headers["x-unrelated"], undefined) - }) }) describe("Check that the properly used query endpoints function and interact with RERUM. __e2e", () => { From a2abc6bf657bfa3dc92badb78cd8967c8ccf3958 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 15 Sep 2026 13:07:16 -0500 Subject: [PATCH 3/4] Remove pagination header section from TESTING.md --- test/TESTING.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/TESTING.md b/test/TESTING.md index cfeb281..6763f9f 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -86,17 +86,6 @@ This prevents accidental changes to: - ✅ Body field name (`isOverwritten` vs other names) - ✅ Precedence between header and body sources -### Pagination Header Forwarding - -RERUM reports the applied and maximum `limit` and `skip` on `/query` responses in the `Pagination-Limit`, `Pagination-Skip`, `Pagination-Limit-Max`, and `Pagination-Skip-Max` headers. The query route forwards them to the client. Tests in query.test.js validate: - -1. **Success path** — every `Pagination-*` header RERUM returns is on the 200 -2. **Error path** — the `Pagination-*` headers RERUM returns on a failure are on the 502 -3. **Absence handling** — no `Pagination-*` headers are added when RERUM sends none, or when RERUM is never reached -4. **No over-forwarding** — unrelated RERUM response headers are not copied - -Fetch mocks in query.test.js must include `headers: new Headers()` because the route reads `resp.headers`. - ### Future Improvements Once RERUM provides a reliable test harness, TinyNode will: From 7411c496d592fc308177d4817ff3c19ab4582cfe Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 15 Sep 2026 13:10:23 -0500 Subject: [PATCH 4/4] deploy to dev --- routes/query.js | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/query.js b/routes/query.js index 11a3097..98d7057 100644 --- a/routes/query.js +++ b/routes/query.js @@ -3,7 +3,6 @@ import { httpError, verifyJsonContentType } from "../rest.js" import { fetchRerum } from "../rerum.js" const router = express.Router() -// RERUM reports the applied and maximum limit and skip in these headers. const PAGINATION_HEADERS = ["Pagination-Limit", "Pagination-Skip", "Pagination-Limit-Max", "Pagination-Skip-Max"] /* POST a query to the thing. */