Skip to content

Commit efd7f97

Browse files
Fix D1 Export error handling when presigned URLs are invalid (#8131)
* Fix D1 Export error handling when presigned URLs are invalid * Update packages/wrangler/src/d1/export.ts Co-authored-by: Max Rozen <3822106+rozenmd@users.noreply.github.com> * fix test * pnpm run prettify * changeset --------- Co-authored-by: Max Rozen <3822106+rozenmd@users.noreply.github.com>
1 parent c563137 commit efd7f97

File tree

3 files changed

+103
-59
lines changed

3 files changed

+103
-59
lines changed

.changeset/olive-gifts-flash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
D1 export will now show an error when the presigned URL is invalid

packages/wrangler/src/__tests__/d1/export.test.ts

+93-59
Original file line numberDiff line numberDiff line change
@@ -114,65 +114,8 @@ describe("export", () => {
114114
]);
115115
const mockSqlContent = "PRAGMA defer_foreign_keys=TRUE;";
116116

117-
msw.use(
118-
http.post(
119-
"*/accounts/:accountId/d1/database/:databaseId/export",
120-
async ({ request }) => {
121-
// This endpoint is polled recursively. If we respond immediately,
122-
// the callstack builds up quickly leading to a hard-to-debug OOM error.
123-
// This timeout ensures that if the endpoint is accidently polled infinitely
124-
// the test will timeout before breaching available memory
125-
await setTimeout(10);
126-
127-
const body = (await request.json()) as Record<string, unknown>;
128-
129-
// First request, initiates a new task
130-
if (!body.current_bookmark) {
131-
return HttpResponse.json(
132-
{
133-
success: true,
134-
result: {
135-
success: true,
136-
type: "export",
137-
at_bookmark: "yyyy",
138-
status: "active",
139-
messages: [
140-
"Generating xxxx-yyyy.sql",
141-
"Uploaded part 2", // out-of-order uploads ok
142-
"Uploaded part 1",
143-
],
144-
},
145-
},
146-
{ status: 202 }
147-
);
148-
}
149-
// Subsequent request, sees that it is complete
150-
else {
151-
return HttpResponse.json(
152-
{
153-
success: true,
154-
result: {
155-
success: true,
156-
type: "export",
157-
at_bookmark: "yyyy",
158-
status: "complete",
159-
result: {
160-
filename: "xxxx-yyyy.sql",
161-
signed_url: "https://example.com/xxxx-yyyy.sql",
162-
},
163-
messages: [
164-
"Uploaded part 3",
165-
"Uploaded part 4",
166-
"Finished uploading xxxx-yyyy.sql in 4 parts.",
167-
],
168-
},
169-
},
170-
{ status: 200 }
171-
);
172-
}
173-
}
174-
)
175-
);
117+
mockResponses();
118+
176119
msw.use(
177120
http.get("https://example.com/xxxx-yyyy.sql", async () => {
178121
return HttpResponse.text(mockSqlContent, { status: 200 });
@@ -182,4 +125,95 @@ describe("export", () => {
182125
await runWrangler("d1 export db --remote --output test-remote.sql");
183126
expect(fs.readFileSync("test-remote.sql", "utf8")).toBe(mockSqlContent);
184127
});
128+
129+
it("should handle remote presigned URL errors", async () => {
130+
setIsTTY(false);
131+
writeWranglerConfig({
132+
d1_databases: [
133+
{ binding: "DATABASE", database_name: "db", database_id: "xxxx" },
134+
],
135+
});
136+
mockGetMemberships([
137+
{ id: "IG-88", account: { id: "1701", name: "enterprise" } },
138+
]);
139+
140+
mockResponses();
141+
142+
msw.use(
143+
http.get("https://example.com/xxxx-yyyy.sql", async () => {
144+
return HttpResponse.text(
145+
`<?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>`,
146+
{ status: 403 }
147+
);
148+
})
149+
);
150+
151+
await expect(
152+
runWrangler("d1 export db --remote --output test-remote.sql")
153+
).rejects.toThrowError(
154+
/There was an error while downloading from the presigned URL with status code: 403/
155+
);
156+
});
185157
});
158+
159+
function mockResponses() {
160+
msw.use(
161+
http.post(
162+
"*/accounts/:accountId/d1/database/:databaseId/export",
163+
async ({ request }) => {
164+
// This endpoint is polled recursively. If we respond immediately,
165+
// the callstack builds up quickly leading to a hard-to-debug OOM error.
166+
// This timeout ensures that if the endpoint is accidently polled infinitely
167+
// the test will timeout before breaching available memory
168+
await setTimeout(10);
169+
170+
const body = (await request.json()) as Record<string, unknown>;
171+
172+
// First request, initiates a new task
173+
if (!body.current_bookmark) {
174+
return HttpResponse.json(
175+
{
176+
success: true,
177+
result: {
178+
success: true,
179+
type: "export",
180+
at_bookmark: "yyyy",
181+
status: "active",
182+
messages: [
183+
"Generating xxxx-yyyy.sql",
184+
"Uploaded part 2", // out-of-order uploads ok
185+
"Uploaded part 1",
186+
],
187+
},
188+
},
189+
{ status: 202 }
190+
);
191+
}
192+
// Subsequent request, sees that it is complete
193+
else {
194+
return HttpResponse.json(
195+
{
196+
success: true,
197+
result: {
198+
success: true,
199+
type: "export",
200+
at_bookmark: "yyyy",
201+
status: "complete",
202+
result: {
203+
filename: "xxxx-yyyy.sql",
204+
signed_url: "https://example.com/xxxx-yyyy.sql",
205+
},
206+
messages: [
207+
"Uploaded part 3",
208+
"Uploaded part 4",
209+
"Finished uploading xxxx-yyyy.sql in 4 parts.",
210+
],
211+
},
212+
},
213+
{ status: 200 }
214+
);
215+
}
216+
}
217+
)
218+
);
219+
}

packages/wrangler/src/d1/export.ts

+5
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ async function exportRemotely(
193193
startMessage: `Downloading SQL to ${output}`,
194194
async promise() {
195195
const contents = await fetch(finalResponse.result.signed_url);
196+
if (!contents.ok) {
197+
throw new Error(
198+
`There was an error while downloading from the presigned URL with status code: ${contents.status}`
199+
);
200+
}
196201
await fs.writeFile(output, contents.body || "");
197202
},
198203
});

0 commit comments

Comments
 (0)