Skip to content

Commit 8003923

Browse files
authored
feat(server): Gracefully handle oversized batch items instead of aborting the stream (#3137)
Gracefully handle oversized batch items instead of aborting the stream. When an NDJSON batch item exceeds the maximum size, the parser now emits an error marker instead of throwing, allowing the batch to seal normally. The oversized item becomes a pre-failed run with `PAYLOAD_TOO_LARGE` error code, while other items in the batch process successfully. This prevents `batchTriggerAndWait` from seeing connection errors and retrying with exponential backoff. Also fixes the NDJSON parser not consuming the remainder of an oversized line split across multiple chunks, which caused "Invalid JSON" errors on subsequent lines.
1 parent cff4566 commit 8003923

File tree

14 files changed

+541
-86
lines changed

14 files changed

+541
-86
lines changed

.changeset/modern-boxes-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
Add PAYLOAD_TOO_LARGE error to handle graceful recovery of sending batch trigger items with payloads that exceed the maximum payload size
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Gracefully handle oversized batch items instead of aborting the stream.
7+
8+
When an NDJSON batch item exceeds the maximum size, the parser now emits an error marker instead of throwing, allowing the batch to seal normally. The oversized item becomes a pre-failed run with `PAYLOAD_TOO_LARGE` error code, while other items in the batch process successfully. This prevents `batchTriggerAndWait` from seeing connection errors and retrying with exponential backoff.
9+
10+
Also fixes the NDJSON parser not consuming the remainder of an oversized line split across multiple chunks, which caused "Invalid JSON" errors on subsequent lines.

ailogger-output.log

Whitespace-only changes.

apps/webapp/app/presenters/v3/SpanPresenter.server.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,7 @@ export class SpanPresenter extends BasePresenter {
509509
taskIdentifier: true,
510510
spanId: true,
511511
createdAt: true,
512-
number: true,
513-
taskVersion: true,
512+
status: true,
514513
},
515514
where: {
516515
parentSpanId: spanId,

apps/webapp/app/routes/api.v3.batches.$batchId.items.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
9999
if (error instanceof ServiceValidationError) {
100100
return json({ error: error.message }, { status: 422 });
101101
} else if (error instanceof Error) {
102-
// Check for stream parsing errors
103-
if (
104-
error.message.includes("Invalid JSON") ||
105-
error.message.includes("exceeds maximum size")
106-
) {
102+
// Check for stream parsing errors (e.g. invalid JSON)
103+
if (error.message.includes("Invalid JSON")) {
107104
return json({ error: error.message }, { status: 400 });
108105
}
109106

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/Ru
5858
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
5959
import { RunIcon } from "~/components/runs/v3/RunIcon";
6060
import { RunTag } from "~/components/runs/v3/RunTag";
61+
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
6162
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
6263
import { SpanTitle } from "~/components/runs/v3/SpanTitle";
6364
import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptStatus";
@@ -133,9 +134,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
133134
name: error.name,
134135
message: error.message,
135136
stack: error.stack,
136-
cause: error.cause instanceof Error
137-
? { name: error.cause.name, message: error.cause.message }
138-
: error.cause,
137+
cause:
138+
error.cause instanceof Error
139+
? { name: error.cause.name, message: error.cause.message }
140+
: error.cause,
139141
}
140142
: error,
141143
});
@@ -1003,7 +1005,7 @@ function RunBody({
10031005
)}
10041006
</div>
10051007
</div>
1006-
<div className="flex items-center flex-wrap py-2 justify-between gap-2 border-t border-grid-dimmed px-2">
1008+
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-grid-dimmed px-2 py-2">
10071009
<div className="flex items-center gap-4">
10081010
{run.friendlyId !== runParam && (
10091011
<LinkButton
@@ -1047,9 +1049,11 @@ function RunBody({
10471049
</PopoverTrigger>
10481050
<PopoverContent className="min-w-[140px] p-1" align="end">
10491051
<PopoverMenuItem
1050-
to={`${v3LogsPath(organization, project, environment)}?runId=${runParam}&from=${
1051-
new Date(run.createdAt).getTime() - 60000
1052-
}`}
1052+
to={`${v3LogsPath(
1053+
organization,
1054+
project,
1055+
environment
1056+
)}?runId=${runParam}&from=${new Date(run.createdAt).getTime() - 60000}`}
10531057
title="View logs"
10541058
icon={ArrowRightIcon}
10551059
leadingIconClassName="text-blue-500"
@@ -1200,50 +1204,6 @@ function SpanEntity({ span }: { span: Span }) {
12001204
<Property.Label>Message</Property.Label>
12011205
<Property.Value className="whitespace-pre-wrap">{span.message}</Property.Value>
12021206
</Property.Item>
1203-
{span.triggeredRuns.length > 0 && (
1204-
<Property.Item>
1205-
<div className="flex flex-col gap-1.5">
1206-
<Header3>Triggered runs</Header3>
1207-
<Table containerClassName="max-h-[12.5rem]">
1208-
<TableHeader className="bg-background-bright">
1209-
<TableRow>
1210-
<TableHeaderCell>Run #</TableHeaderCell>
1211-
<TableHeaderCell>Task</TableHeaderCell>
1212-
<TableHeaderCell>Version</TableHeaderCell>
1213-
<TableHeaderCell>Created at</TableHeaderCell>
1214-
</TableRow>
1215-
</TableHeader>
1216-
<TableBody>
1217-
{span.triggeredRuns.map((run) => {
1218-
const path = v3RunSpanPath(
1219-
organization,
1220-
project,
1221-
environment,
1222-
{ friendlyId: run.friendlyId },
1223-
{ spanId: run.spanId }
1224-
);
1225-
return (
1226-
<TableRow key={run.friendlyId}>
1227-
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1228-
{run.number}
1229-
</TableCell>
1230-
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1231-
{run.taskIdentifier}
1232-
</TableCell>
1233-
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1234-
{run.taskVersion ?? "–"}
1235-
</TableCell>
1236-
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1237-
<DateTime date={run.createdAt} />
1238-
</TableCell>
1239-
</TableRow>
1240-
);
1241-
})}
1242-
</TableBody>
1243-
</Table>
1244-
</div>
1245-
</Property.Item>
1246-
)}
12471207
</Property.Table>
12481208
{span.events.length > 0 && <SpanEvents spanEvents={span.events} />}
12491209
{span.properties !== undefined ? (
@@ -1268,6 +1228,48 @@ function SpanEntity({ span }: { span: Span }) {
12681228
showOpenInModal
12691229
/>
12701230
) : null}
1231+
{span.triggeredRuns.length > 0 && (
1232+
<div className="flex flex-col gap-1.5">
1233+
<Header3>Runs</Header3>
1234+
<Table containerClassName="max-h-[12.5rem]">
1235+
<TableHeader className="bg-background-bright">
1236+
<TableRow>
1237+
<TableHeaderCell>ID</TableHeaderCell>
1238+
<TableHeaderCell>Task</TableHeaderCell>
1239+
<TableHeaderCell>Status</TableHeaderCell>
1240+
<TableHeaderCell>Created</TableHeaderCell>
1241+
</TableRow>
1242+
</TableHeader>
1243+
<TableBody>
1244+
{span.triggeredRuns.map((run) => {
1245+
const path = v3RunSpanPath(
1246+
organization,
1247+
project,
1248+
environment,
1249+
{ friendlyId: run.friendlyId },
1250+
{ spanId: run.spanId }
1251+
);
1252+
return (
1253+
<TableRow key={run.friendlyId}>
1254+
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1255+
<TruncatedCopyableValue value={run.friendlyId} />
1256+
</TableCell>
1257+
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1258+
{run.taskIdentifier}
1259+
</TableCell>
1260+
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1261+
<TaskRunStatusCombo status={run.status} />
1262+
</TableCell>
1263+
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
1264+
<DateTimeAccurate date={run.createdAt} hour12={false} hideDate={true} />
1265+
</TableCell>
1266+
</TableRow>
1267+
);
1268+
})}
1269+
</TableBody>
1270+
</Table>
1271+
</div>
1272+
)}
12711273
</div>
12721274
);
12731275
}

0 commit comments

Comments
 (0)