From 9325dd7e79afdfdeb3db210eb9cb8134ffe7756f Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Sat, 10 Sep 2022 09:57:40 +0200 Subject: [PATCH] Handle internal errors for creating containers Workaround for an NSS bug https://github.com/nodeSolidServer/node-solid-server/issues/1703 --- src/solid/SolidClient.test.ts | 75 ++++++++++++++++++++++++++++++++++- src/solid/SolidClient.ts | 54 ++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/solid/SolidClient.test.ts b/src/solid/SolidClient.test.ts index 2e1b9bb..598d6ab 100644 --- a/src/solid/SolidClient.test.ts +++ b/src/solid/SolidClient.test.ts @@ -13,7 +13,7 @@ import UpdatePropertyOperation from '@/solid/operations/UpdatePropertyOperation' import type RDFDocument from '@/solid/RDFDocument'; import StubFetcher from '@/testing/lib/stubs/StubFetcher'; -import { fakeResourceUrl } from '@/testing/utils'; +import { fakeContainerUrl, fakeResourceUrl } from '@/testing/utils'; describe('SolidClient', () => { @@ -130,6 +130,79 @@ describe('SolidClient', () => { }); }); + it('falls back to creating container documents using POST', async () => { + // Arrange + const grandParentSlug = stringToSlug(Faker.random.word()); + const parentSlug = stringToSlug(Faker.random.word()); + const label = Faker.random.word(); + const rootUrl = fakeContainerUrl(); + const grandParentUrl = urlResolveDirectory(rootUrl, grandParentSlug); + const parentUrl = urlResolveDirectory(grandParentUrl, parentSlug); + const containerUrl = urlResolveDirectory(parentUrl, stringToSlug(label)); + + StubFetcher.addFetchResponse('', {}, 500); // PATCH new container + StubFetcher.addFetchResponse('', {}, 404); // POST new container + StubFetcher.addFetchResponse('', {}, 404); // POST parent + StubFetcher.addFetchResponse('', {}, 201); // POST grandparent + StubFetcher.addFetchResponse('', {}, 201); // POST parent + StubFetcher.addFetchResponse('', {}, 201); // POST new container + + // Act + const url = await client.createDocument( + parentUrl, + containerUrl, + [ + RDFResourceProperty.literal(containerUrl, IRI('rdfs:label'), label), + RDFResourceProperty.literal(containerUrl, IRI('purl:modified'), new Date()), + RDFResourceProperty.type(containerUrl, IRI('ldp:Container')), + ], + ); + + // Assert + expect(url).toEqual(containerUrl); + expect(StubFetcher.fetch).toHaveBeenCalledTimes(6); + + [1, 5].forEach(index => { + expect(StubFetcher.fetchSpy.mock.calls[index]?.[0]).toEqual(parentUrl); + expect(StubFetcher.fetchSpy.mock.calls[index]?.[1]).toEqual({ + method: 'POST', + headers: { + 'Content-Type': 'text/turtle', + 'Link': '; rel="type"', + 'Slug': stringToSlug(label), + 'If-None-Match': '*', + }, + body: `<> "${label}" .`, + }); + }); + + [2, 4].forEach(index => { + expect(StubFetcher.fetchSpy.mock.calls[index]?.[0]).toEqual(grandParentUrl); + expect(StubFetcher.fetchSpy.mock.calls[index]?.[1]).toEqual({ + method: 'POST', + headers: { + 'Content-Type': 'text/turtle', + 'Link': '; rel="type"', + 'Slug': parentSlug, + 'If-None-Match': '*', + }, + body: '', + }); + }); + + expect(StubFetcher.fetchSpy.mock.calls[3]?.[0]).toEqual(rootUrl); + expect(StubFetcher.fetchSpy.mock.calls[3]?.[1]).toEqual({ + method: 'POST', + headers: { + 'Content-Type': 'text/turtle', + 'Link': '; rel="type"', + 'Slug': grandParentSlug, + 'If-None-Match': '*', + }, + body: '', + }); + }); + it('gets one document', async () => { // Arrange const url = Faker.internet.url(); diff --git a/src/solid/SolidClient.ts b/src/solid/SolidClient.ts index 6a530c9..d089940 100644 --- a/src/solid/SolidClient.ts +++ b/src/solid/SolidClient.ts @@ -3,9 +3,9 @@ import { arrayDiff, arrayFilter, arrayRemove, - objectWithoutEmpty, + requireUrlDirectoryName, + requireUrlParentDirectory, urlClean, - urlDirectoryName, urlResolve, } from '@noeldemartin/utils'; import { NetworkRequestError, UnsuccessfulNetworkRequestError } from '@noeldemartin/solid-utils'; @@ -236,21 +236,54 @@ export default class SolidClient { url, { method: 'PATCH', - headers: objectWithoutEmpty({ + headers: { 'Content-Type': 'application/sparql-update', 'Link': '; rel="type"', - 'Slug': urlDirectoryName(url), + 'Slug': requireUrlDirectoryName(url), 'If-None-Match': '*', - }) as Record, + }, body: `INSERT DATA { ${turtle} }`, }, ); + // Handle NSS internal error + // See https://github.com/nodeSolidServer/node-solid-server/issues/1703 + await this.handleInternalErrorResponse(response, () => this.createContainerDocumentUsingPOST(url, turtle)); + this.assertSuccessfulResponse(response, `Error creating container at ${url}`); return url; } + private async createContainerDocumentUsingPOST( + url: string, + turtle: string, + createParent: boolean = true, + ): Promise { + const parentUrl = requireUrlParentDirectory(url); + const response = await this.fetch( + parentUrl, + { + method: 'POST', + headers: { + 'Content-Type': 'text/turtle', + 'Link': '; rel="type"', + 'Slug': requireUrlDirectoryName(url), + 'If-None-Match': '*', + }, + body: turtle, + }, + ); + + if (response.status === 404 && createParent) { + await this.createContainerDocumentUsingPOST(parentUrl, ''); + + return this.createContainerDocumentUsingPOST(url, turtle, false); + } + + this.assertSuccessfulResponse(response, `Error creating container at ${url}`); + } + private async createNonContainerDocument( parentUrl: string, url: string | null, @@ -517,10 +550,19 @@ export default class SolidClient { } private assertSuccessfulResponse(response: Response, errorMessage: string): void { - if (Math.floor(response.status / 100) === 2) + if (Math.floor(response.status / 100) === 2) { return; + } throw new UnsuccessfulNetworkRequestError(errorMessage, response); } + private async handleInternalErrorResponse(response: Response, callback: () => Promise): Promise { + if (Math.floor(response.status / 100) !== 5) { + return; + } + + await callback(); + } + }