Skip to content

Commit fa9a792

Browse files
committed
fix(linear): guard matchEvent and formatInput against a null body
Found during a final backwards-compat pass, same class of bug Greptile just caught in extractIdempotencyId: both cast body straight to a Record without checking it was actually an object, so a genuinely null/malformed body would throw instead of degrading gracefully. gitlab.ts (asRecord's `|| {}`) and instantly.ts (isRecordLike checks) already guard this everywhere; linear.ts was the one inconsistent provider. Uses the same isRecordLike helper as instantly.ts.
1 parent 01ac230 commit fa9a792

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

apps/sim/lib/webhooks/providers/linear.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ describe('Linear webhook provider', () => {
111111
expect(res).toBeNull()
112112
})
113113

114+
describe('matchEvent', () => {
115+
it('returns null-body-safe result instead of throwing when body is null', async () => {
116+
const result = await linearHandler.matchEvent!({
117+
body: null,
118+
requestId: 'linear-t6',
119+
providerConfig: { triggerId: 'linear_issue_created' },
120+
webhook: {},
121+
workflow: {},
122+
request: new NextRequest('http://localhost/test'),
123+
})
124+
expect(result).toBe(false)
125+
})
126+
})
127+
128+
describe('formatInput', () => {
129+
it('does not throw when body is null', async () => {
130+
const { input } = await linearHandler.formatInput!({ body: null } as any)
131+
expect(input).toMatchObject({ action: '', type: '', actor: null })
132+
})
133+
})
134+
114135
describe('extractIdempotencyId', () => {
115136
it('builds a stable key from type, action, entity id, and updatedAt', () => {
116137
const key = linearHandler.extractIdempotencyId!({

apps/sim/lib/webhooks/providers/linear.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { safeCompare } from '@sim/security/compare'
33
import { hmacSha256Hex } from '@sim/security/hmac'
44
import { toError } from '@sim/utils/errors'
55
import { generateId } from '@sim/utils/id'
6+
import { isRecordLike } from '@sim/utils/object'
67
import { NextResponse } from 'next/server'
78
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
89
import type {
@@ -106,7 +107,7 @@ export const linearHandler: WebhookProviderHandler = {
106107
},
107108

108109
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
109-
const b = body as Record<string, unknown>
110+
const b = isRecordLike(body) ? body : {}
110111
const rawActor = b.actor
111112
let actor: unknown = null
112113
if (rawActor && typeof rawActor === 'object' && !Array.isArray(rawActor)) {
@@ -138,9 +139,9 @@ export const linearHandler: WebhookProviderHandler = {
138139
const triggerId = providerConfig.triggerId as string | undefined
139140
if (triggerId && !triggerId.endsWith('_webhook') && !triggerId.endsWith('_webhook_v2')) {
140141
const { isLinearEventMatch } = await import('@/triggers/linear/utils')
141-
const obj = body as Record<string, unknown>
142-
const action = obj.action as string | undefined
143-
const type = obj.type as string | undefined
142+
const obj = isRecordLike(body) ? body : {}
143+
const action = typeof obj.action === 'string' ? obj.action : undefined
144+
const type = typeof obj.type === 'string' ? obj.type : undefined
144145
if (!isLinearEventMatch(triggerId, type || '', action)) {
145146
logger.debug(
146147
`[${requestId}] Linear event mismatch for trigger ${triggerId}. Type: ${type}, Action: ${action}. Skipping.`

0 commit comments

Comments
 (0)