Skip to content

Commit f831f9a

Browse files
fix: prevent submission endpoint hang on Cloudflare Workers (#141)
Replace the interactive transaction in formData.setFormData with a single atomic ctx.db.batch(). The interactive transaction issued four sequential network round-trips (BEGIN/INSERT/UPDATE/COMMIT) that held the libSQL write lock for their whole duration; under concurrent submissions they serialized on the lock and blew the Worker execution budget, so workerd canceled the request as hung. batch() runs both writes in one round-trip inside an implicit backend transaction, preserving all-or-nothing atomicity (the FK-failure rollback test stays green). Defer the notification email with Next's after() instead of a floating promise. I/O started by a promise that outlives the response is invalid on Workers, so notification emails silently failed; after() runs it in a valid post-response context (OpenNext maps it to waitUntil) and a .catch() keeps a failed email from escalating to a request exception.
1 parent cd56a6f commit f831f9a

2 files changed

Lines changed: 13 additions & 10 deletions

File tree

‎apps/web/src/app/api/s/[id]/route.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { userAgent } from 'next/server';
1+
import { after, userAgent } from 'next/server';
22

33
import { type RouterOutputs } from '@formbase/api';
44

@@ -142,7 +142,11 @@ export async function POST(
142142
});
143143

144144
if (!spamResult.isSpam) {
145-
void handleEmailNotifications(form, cleanedFormData);
145+
after(() =>
146+
handleEmailNotifications(form, cleanedFormData).catch((error) => {
147+
console.error('Failed to send submission notification email', error);
148+
}),
149+
);
146150
}
147151
const { browser } = userAgent(request);
148152

‎packages/api/routers/formData.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,25 @@ export const formDataRouter = createTRPCRouter({
120120
}),
121121
)
122122
.mutation(async ({ ctx, input }) => {
123-
return await ctx.db.transaction(async (tx) => {
124-
const formdata = await tx.insert(formDatas).values({
123+
const [formdata] = await ctx.db.batch([
124+
ctx.db.insert(formDatas).values({
125125
data: serializeJson(input.data),
126126
formId: input.formId,
127127
id: generateId(15),
128128
createdAt: new Date(),
129129
isSpam: input.isSpam,
130130
spamReason: input.spamReason ?? null,
131-
});
132-
133-
await tx
131+
}),
132+
ctx.db
134133
.update(forms)
135134
.set({
136135
updatedAt: new Date(),
137136
keys: serializeJson(input.keys),
138137
})
139-
.where(eq(forms.id, input.formId));
138+
.where(eq(forms.id, input.formId)),
139+
]);
140140

141-
return formdata;
142-
});
141+
return formdata;
143142
}),
144143

145144
markAsSpam: protectedProcedure

0 commit comments

Comments
 (0)