Skip to content

Commit

Permalink
Add NSS workaround for createEmptyContainerAt()
Browse files Browse the repository at this point in the history
Unfortunately it does not support the spec-supported method of
creating a Container:
nodeSolidServer/node-solid-server#1465

As a workaround, we create a dummy file within the Container we
want to create, which should lead to the server creating the
Container on the fly. The dummy file is then immediately deleted
again.

Although this method theoretically works for other servers as well,
it is a bit cumbersome and more error-prone due to the multiple
consecutive requests. Therefore I decided to use it specifically
when we detect NSS's message about not supporting the
spec-prescribed method of creating a Container, so that it can be
easily removed wholesale once NSS is no longer a problem, and the
implementation does not surprise other contributors.
  • Loading branch information
Vinnl committed Aug 18, 2020
1 parent 243a6f1 commit 0219b01
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 0 deletions.
144 changes: 144 additions & 0 deletions src/resource/solidDataset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,150 @@ describe("createEmptyContainerAt", () => {
new Error("Creating the empty Container failed: 403 Forbidden.")
);
});

describe("using the workaround for Node Solid Server", () => {
it("creates and deletes a dummy file inside the Container when encountering NSS's exact error message", async () => {
const mockFetch = jest
.fn(window.fetch)
// Trying to create a Container the regular way:
.mockReturnValueOnce(
Promise.resolve(
new Response(
"Can't write file: PUT not supported on containers, use POST instead",
{ status: 409 }
)
)
)
// Creating a dummy file:
.mockReturnValueOnce(
Promise.resolve(new Response("Creation successful.", { status: 200 }))
)
// Deleting that dummy file:
.mockReturnValueOnce(
Promise.resolve(new Response("Deletion successful", { status: 200 }))
)
// Getting the Container's metadata:
.mockReturnValueOnce(
Promise.resolve(new Response(undefined, { status: 200 }))
);

await createEmptyContainerAt("https://arbitrary.pod/container/", {
fetch: mockFetch,
});

expect(mockFetch.mock.calls).toHaveLength(4);
expect(mockFetch.mock.calls[1][0]).toBe(
"https://arbitrary.pod/container/.dummy"
);
expect(mockFetch.mock.calls[1][1]?.method).toBe("PUT");
expect(mockFetch.mock.calls[2][0]).toBe(
"https://arbitrary.pod/container/.dummy"
);
expect(mockFetch.mock.calls[2][1]?.method).toBe("DELETE");
expect(mockFetch.mock.calls[3][0]).toBe(
"https://arbitrary.pod/container/"
);
expect(mockFetch.mock.calls[3][1]?.method).toBe("HEAD");
});

it("does not attempt to create a dummy file on a regular 409 error", async () => {
const mockFetch = jest
.fn(window.fetch)
.mockReturnValue(
Promise.resolve(
new Response(
"This is a perfectly regular 409 error that has nothing to do with our not supporting the spec.",
{ status: 409 }
)
)
);

const fetchPromise = createEmptyContainerAt(
"https://arbitrary.pod/container/",
{
fetch: mockFetch,
}
);

await expect(fetchPromise).rejects.toThrow(
new Error("Creating the empty Container failed: 409 Conflict.")
);
expect(mockFetch.mock.calls).toHaveLength(1);
});

it("appends a trailing slash if not provided", async () => {
const mockFetch = jest
.fn(window.fetch)
// Trying to create a Container the regular way:
.mockReturnValueOnce(
Promise.resolve(
new Response(
"Can't write file: PUT not supported on containers, use POST instead",
{ status: 409 }
)
)
)
// Creating a dummy file:
.mockReturnValueOnce(
Promise.resolve(new Response("Creation successful.", { status: 200 }))
)
// Deleting that dummy file:
.mockReturnValueOnce(
Promise.resolve(new Response("Deletion successful", { status: 200 }))
)
// Getting the Container's metadata:
.mockReturnValueOnce(
Promise.resolve(new Response(undefined, { status: 200 }))
);

await createEmptyContainerAt("https://arbitrary.pod/container", {
fetch: mockFetch,
});

expect(mockFetch.mock.calls).toHaveLength(4);
expect(mockFetch.mock.calls[1][0]).toBe(
"https://arbitrary.pod/container/.dummy"
);
expect(mockFetch.mock.calls[1][1]?.method).toBe("PUT");
expect(mockFetch.mock.calls[2][0]).toBe(
"https://arbitrary.pod/container/.dummy"
);
expect(mockFetch.mock.calls[2][1]?.method).toBe("DELETE");
expect(mockFetch.mock.calls[3][0]).toBe(
"https://arbitrary.pod/container/"
);
expect(mockFetch.mock.calls[3][1]?.method).toBe("HEAD");
});

it("returns a meaningful error when the server returns a 403 creating the dummy file", async () => {
const mockFetch = jest
.fn(window.fetch)
// Trying to create a Container the regular way:
.mockReturnValueOnce(
Promise.resolve(
new Response(
"Can't write file: PUT not supported on containers, use POST instead",
{ status: 409 }
)
)
)
// Creating a dummy file:
.mockReturnValueOnce(
Promise.resolve(new Response("Forbidden", { status: 403 }))
);

const fetchPromise = createEmptyContainerAt(
"https://arbitrary.pod/container/",
{
fetch: mockFetch,
}
);

await expect(fetchPromise).rejects.toThrow(
new Error("Creating the empty Container failed: 403 Forbidden.")
);
});
});
});

describe("saveSolidDatasetInContainer", () => {
Expand Down
57 changes: 57 additions & 0 deletions src/resource/solidDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ export async function createEmptyContainerAt(
});

if (!response.ok) {
if (
response.status === 409 &&
response.statusText === "Conflict" &&
(await response.text()).trim() ===
"Can't write file: PUT not supported on containers, use POST instead"
) {
return createEmptyContainerWithNssWorkaroundAt(url, options);
}

throw new Error(
`Creating the empty Container failed: ${response.status} ${response.statusText}.`
);
Expand All @@ -271,6 +280,54 @@ export async function createEmptyContainerAt(
return containerDataset;
}

/**
* Unfortunately Node Solid Server does not confirm to the Solid spec when it comes to Container
* creation. As a workaround, we create a dummy file _inside_ the desired Container (which should
* create the desired Container on the fly), and then delete it again.
*
* @see https://github.com/solid/node-solid-server/issues/1465
*/
const createEmptyContainerWithNssWorkaroundAt: typeof createEmptyContainerAt = async (
url,
options
) => {
url = internal_toIriString(url);
const config = {
...internal_defaultFetchOptions,
...options,
};

const dummyUrl = url + ".dummy";

const createResponse = await config.fetch(dummyUrl, {
method: "PUT",
headers: {
Accept: "text/turtle",
"Content-Type": "text/turtle",
},
});

if (!createResponse.ok) {
throw new Error(
`Creating the empty Container failed: ${createResponse.status} ${createResponse.statusText}.`
);
}

await config.fetch(dummyUrl, { method: "DELETE" });

const containerInfoResponse = await config.fetch(url, { method: "HEAD" });

const resourceInfo = internal_parseResourceInfo(containerInfoResponse);
const containerDataset: SolidDataset &
WithChangeLog &
WithResourceInfo = Object.assign(dataset(), {
internal_changeLog: { additions: [], deletions: [] },
internal_resourceInfo: resourceInfo,
});

return containerDataset;
};

function isUpdate(
solidDataset: SolidDataset,
url: UrlString
Expand Down

0 comments on commit 0219b01

Please sign in to comment.