From cee3cd75d4f667a9656b51e28f2696736a8b0baa Mon Sep 17 00:00:00 2001 From: owen Date: Tue, 23 Jun 2026 19:53:57 -0400 Subject: [PATCH] Fix PGCR route decode for plain JSON storage (API-1N). Services stores uncompressed JSON in pgcr; always gunzipSync caused incorrect header check errors on /pgcr/:instanceId. Co-authored-by: Cursor --- src/routes/pgcr.ts | 8 ++---- src/services/pgcr.test.ts | 51 ++++++++++----------------------------- src/services/pgcr.ts | 16 ++++++++++++ 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/routes/pgcr.ts b/src/routes/pgcr.ts index 4c1a362a..69e55cf3 100644 --- a/src/routes/pgcr.ts +++ b/src/routes/pgcr.ts @@ -7,12 +7,9 @@ import { import { ErrorCode } from "@/schema/errors/ErrorCode" import { zBigIntString } from "@/schema/input" import { zInt64 } from "@/schema/output" -import { getRawCompressedPGCR } from "@/services/pgcr" -import { gunzipSync } from "bun" +import { decodePgcrPayload, getRawCompressedPGCR } from "@/services/pgcr" import { z } from "zod" -const decoder = new TextDecoder() - export const pgcrRoute = new RaidHubRoute({ method: "get", description: `Get a raw post game carnage report by instanceId. @@ -46,8 +43,7 @@ Useful if you need to access PGCRs when Bungie's API is down.`, return RaidHubRoute.fail(ErrorCode.PGCRNotFoundError, { instanceId }) } - const decompressed = gunzipSync(result.data) - const pgcr = JSON.parse(decoder.decode(decompressed)) as RaidHubPostGameCarnageReport + const pgcr = JSON.parse(decodePgcrPayload(result.data)) as RaidHubPostGameCarnageReport pgcr.activityDetails.instanceId = BigInt(pgcr.activityDetails.instanceId) pgcr.entries.forEach(entry => { entry.characterId = BigInt(entry.characterId) diff --git a/src/services/pgcr.test.ts b/src/services/pgcr.test.ts index 028d069b..957065b7 100644 --- a/src/services/pgcr.test.ts +++ b/src/services/pgcr.test.ts @@ -1,43 +1,18 @@ -import { getFixturePool } from "@/lib/test-fixture-db" -import { gzipPgcrJson } from "@/lib/test-minimal-pgcr" -import { afterAll, beforeAll, describe, expect, test } from "bun:test" -import { z } from "zod" -import { getRawCompressedPGCR } from "./pgcr" +import { buildMinimalRaidHubPgcrJson, gzipPgcrJson } from "@/lib/test-minimal-pgcr" +import { describe, expect, test } from "bun:test" +import { decodePgcrPayload } from "./pgcr" -const fixtureDb = getFixturePool() -const fixturePgcrInstanceId = "999000000703" +describe("decodePgcrPayload", () => { + const instanceId = "999000000704" -beforeAll(async () => { - await fixtureDb.query(`DELETE FROM raw.pgcr WHERE instance_id = $1::bigint`, [ - fixturePgcrInstanceId - ]) - await fixtureDb.query( - `INSERT INTO raw.pgcr (instance_id, data, date_crawled) VALUES ($1::bigint, $2, NOW())`, - [fixturePgcrInstanceId, gzipPgcrJson(fixturePgcrInstanceId)] - ) -}) - -afterAll(async () => { - await fixtureDb.query(`DELETE FROM raw.pgcr WHERE instance_id = $1::bigint`, [ - fixturePgcrInstanceId - ]) -}) - -describe("getRawCompressedPGCR", () => { - test("returns the correct shape", async () => { - const data = await getRawCompressedPGCR(fixturePgcrInstanceId).catch(console.error) + test("decodes gzip-compressed JSON", () => { + const json = decodePgcrPayload(gzipPgcrJson(instanceId)) + expect(JSON.parse(json).activityDetails.instanceId).toBe(instanceId) + }) - const parsed = z - .object({ - data: z.instanceof(Buffer) - }) - .strict() - .safeParse(data) - if (!parsed.success) { - console.error(parsed.error.errors) - expect(parsed.error.errors).toEqual([]) - } else { - expect(parsed.success).toBe(true) - } + test("decodes plain JSON", () => { + const plain = Buffer.from(JSON.stringify(buildMinimalRaidHubPgcrJson(instanceId))) + const json = decodePgcrPayload(plain) + expect(JSON.parse(json).activityDetails.instanceId).toBe(instanceId) }) }) diff --git a/src/services/pgcr.ts b/src/services/pgcr.ts index 6213fca5..7fd198d3 100644 --- a/src/services/pgcr.ts +++ b/src/services/pgcr.ts @@ -1,4 +1,20 @@ import { pgReader } from "@/integrations/postgres" +import { gunzipSync } from "bun" + +const decoder = new TextDecoder() + +/** Gzip magic bytes — matches Services `log-raw-pgcr` and Hermes storage (plain JSON when absent). */ +function isGzipCompressed(data: Buffer): boolean { + return data.length >= 2 && data[0] === 0x1f && data[1] === 0x8b +} + +/** Decode PGCR payload stored as gzip (legacy raw.pgcr) or plain JSON (pgcr table). */ +export function decodePgcrPayload(data: Buffer): string { + if (isGzipCompressed(data)) { + return decoder.decode(gunzipSync(data)) + } + return decoder.decode(data) +} export async function getRawCompressedPGCR(instanceId: bigint | string) { return await pgReader.queryRow<{