Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
isFileDefInstance,
isFileMetaResource,
isSingleCardDocument,
isSingleFileMetaDocument,
isLinkableCollectionDocument,
resolveFileDefCodeRef,
X_BOXEL_JOB_PRIORITY_HEADER,
Expand Down Expand Up @@ -1836,6 +1837,23 @@ export default class StoreService extends Service implements StoreInterface {
json = await this.cardService.fetchJSON(url);
}
if (!isSingleCardDocument(json)) {
// The URL turned out to be a binary file (e.g. an uploaded
// image). The realm-server returns a file-meta JSON document
// in that case; reroute to the file-meta load path so the
// caller gets a FileDef instead of a hard failure.
if (isSingleFileMetaDocument(json)) {
// URL was a binary file; reroute to the file-meta bucket.
let fileMeta = await this.getFileMetaInstance<FileDef>({
idOrDoc: url,
opts: {
noCache: opts?.noCache,
dependencyTrackingContext: opts?.dependencyTrackingContext,
},
});
// Resolve inflightGetCards so concurrent callers don't hang.
deferred?.fulfill(fileMeta as unknown as T | CardErrorJSONAPI);
return fileMeta as unknown as T;
}
throw new Error(
`bug: server returned a non card document for ${url}:
${JSON.stringify(json, null, 2)}`,
Expand Down
20 changes: 12 additions & 8 deletions packages/host/tests/integration/store-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -548,27 +548,31 @@ module('Integration | Store', function (hooks) {
);
});

test('file-meta reads do not reuse card errors for the same id', async function (assert) {
test('card read of a binary file URL is rerouted to file-meta', async function (assert) {
await testRealm.write('hero.png', 'mock hero image');
let fileUrl = `${testRealmURL}hero.png`;

let cardError = await storeService.get(fileUrl);
assert.false(
isCardInstance(cardError),
'card read returns an error for file url',
let result = await storeService.get(fileUrl);
assert.ok(
(result as any).constructor?.isFileDef,
'card read returns a FileDef via the safety-net reroute',
);

let fileInstance = await storeService.get(fileUrl, { type: 'file-meta' });
assert.ok(
(fileInstance as any).constructor?.isFileDef,
'file meta instance is a FileDef',
'file-meta read returns a FileDef',
);

assert.ok(storeService.peekError(fileUrl), 'card error remains cached');
assert.strictEqual(
storeService.peekError(fileUrl),
undefined,
'no error cached on the card bucket',
);
assert.strictEqual(
storeService.peekError(fileUrl, { type: 'file-meta' }),
undefined,
'file-meta error cache remains clear',
'no error cached on the file-meta bucket',
);
});

Expand Down
29 changes: 24 additions & 5 deletions packages/realm-server/tests/card-endpoints-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4271,18 +4271,37 @@ module(basename(__filename), function () {
onRealmSetup,
});

test('rejects HTTP requests to file URLs', async function (assert) {
let response;
response = await request
test('GET on a file URL with a card+json Accept returns a file-meta JSON document', async function (assert) {
let response = await request
.get('/greeting.txt')
.set('Accept', 'application/vnd.card+json');

assert.strictEqual(
response.status,
415,
'rejects GET for a file URL with 415 status',
200,
'GET serves a file-meta document instead of 415',
);
assert.true(
(response.headers['content-type'] ?? '').startsWith(
'application/vnd.card+json',
),
'response is JSON, not raw file bytes',
);
let doc = JSON.parse(response.text);
assert.strictEqual(
doc?.data?.type,
'file-meta',
'data.type identifies the resource as file-meta',
);
assert.strictEqual(
doc?.data?.attributes?.name,
'greeting.txt',
'attributes.name carries the file name',
);
});

test('rejects write requests to file URLs', async function (assert) {
let response;
response = await request
.patch('/greeting.txt')
.send({
Expand Down
19 changes: 17 additions & 2 deletions packages/runtime-common/realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4702,7 +4702,17 @@ export class Realm {
});
if (instanceEntry === undefined) {
if (await this.nonJsonFileExists(localPath)) {
return unsupportedMediaType(request, requestContext);
// A path that points to a non-JSON file (e.g. an uploaded
// binary) was asked for as card+json. Return a file-meta JSON
// document so the caller receives valid JSON it can
// discriminate via `data.type === 'file-meta'` — instead of
// raw binary bytes that crash a downstream `response.json()`.
let fileMeta = await this.fileMetaDocument(
requestContext,
localPath,
SupportedMimeType.CardJson,
);
return fileMeta ?? notFound(request, requestContext);
} else {
return notFound(request, requestContext);
}
Expand Down Expand Up @@ -4746,7 +4756,12 @@ export class Realm {
});
if (maybeError === undefined) {
if (await this.nonJsonFileExists(localPath)) {
return unsupportedMediaType(request, requestContext);
let fileMeta = await this.fileMetaDocument(
requestContext,
localPath,
SupportedMimeType.CardJson,
);
return fileMeta ?? notFound(request, requestContext);
} else {
return notFound(request, requestContext);
}
Expand Down
Loading