Skip to content

Commit

Permalink
Handle internal errors for creating containers
Browse files Browse the repository at this point in the history
Workaround for an NSS bug nodeSolidServer/node-solid-server#1703
  • Loading branch information
NoelDeMartin committed Sep 10, 2022
1 parent 0f2ae91 commit 9325dd7
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 7 deletions.
75 changes: 74 additions & 1 deletion src/solid/SolidClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand Down Expand Up @@ -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': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
'Slug': stringToSlug(label),
'If-None-Match': '*',
},
body: `<> <http://www.w3.org/2000/01/rdf-schema#label> "${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': '<http://www.w3.org/ns/ldp#BasicContainer>; 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': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
'Slug': grandParentSlug,
'If-None-Match': '*',
},
body: '',
});
});

it('gets one document', async () => {
// Arrange
const url = Faker.internet.url();
Expand Down
54 changes: 48 additions & 6 deletions src/solid/SolidClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {
arrayDiff,
arrayFilter,
arrayRemove,
objectWithoutEmpty,
requireUrlDirectoryName,
requireUrlParentDirectory,
urlClean,
urlDirectoryName,
urlResolve,
} from '@noeldemartin/utils';
import { NetworkRequestError, UnsuccessfulNetworkRequestError } from '@noeldemartin/solid-utils';
Expand Down Expand Up @@ -236,21 +236,54 @@ export default class SolidClient {
url,
{
method: 'PATCH',
headers: objectWithoutEmpty({
headers: {
'Content-Type': 'application/sparql-update',
'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
'Slug': urlDirectoryName(url),
'Slug': requireUrlDirectoryName(url),
'If-None-Match': '*',
}) as Record<string, string>,
},
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<void> {
const parentUrl = requireUrlParentDirectory(url);
const response = await this.fetch(
parentUrl,
{
method: 'POST',
headers: {
'Content-Type': 'text/turtle',
'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; 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,
Expand Down Expand Up @@ -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<void>): Promise<void> {
if (Math.floor(response.status / 100) !== 5) {
return;
}

await callback();
}

}

0 comments on commit 9325dd7

Please sign in to comment.