Skip to content

Commit d215550

Browse files
committed
fix(uptimerobot): address review — heartbeat URL, file/JSON edge cases
- Block: URL is not required for HEARTBEAT monitors (no URL) - buildMonitorBody: throw on malformed assignedAlertContacts/customHttpHeaders JSON instead of silently dropping the field - PSP route: error (400) when a supplied logo/icon cannot be resolved to a stored file instead of silently omitting the image - PSP route: guard success-path JSON parsing; return a controlled 502 on a non-JSON provider response instead of an uncaught 500
1 parent 3eaaeae commit d215550

3 files changed

Lines changed: 44 additions & 10 deletions

File tree

apps/sim/app/api/tools/uptimerobot/server-utils.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ interface PspFormFields {
2222
* Appends a single optional image file (logo or icon) to the form after
2323
* downloading it from storage and verifying the caller may access it.
2424
*
25-
* @returns an error `NextResponse` if access is denied, otherwise `null`.
25+
* @returns an error `NextResponse` if the file is invalid or access is denied,
26+
* otherwise `null`.
2627
*/
2728
async function appendPspImage(
2829
form: FormData,
@@ -33,7 +34,14 @@ async function appendPspImage(
3334
logger: Logger
3435
): Promise<NextResponse | null> {
3536
const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger)
36-
if (userFiles.length === 0) return null
37+
if (userFiles.length === 0) {
38+
// A file was supplied but could not be resolved to a stored UserFile (e.g. a
39+
// bare string reference). Surface it rather than silently dropping the image.
40+
return NextResponse.json(
41+
{ success: false, error: `Invalid ${field} file: expected an uploaded file reference` },
42+
{ status: 400 }
43+
)
44+
}
3745

3846
const userFile = userFiles[0]
3947
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
@@ -110,6 +118,18 @@ export async function forwardPspRequest(options: {
110118
)
111119
}
112120

113-
const data = text ? JSON.parse(text) : {}
121+
let data: Record<string, unknown> = {}
122+
if (text) {
123+
try {
124+
const parsed = JSON.parse(text)
125+
if (parsed && typeof parsed === 'object') data = parsed as Record<string, unknown>
126+
} catch {
127+
logger.error(`[${requestId}] UptimeRobot returned a non-JSON PSP response`, { body: text })
128+
return NextResponse.json(
129+
{ success: false, error: 'UptimeRobot returned an unexpected response' },
130+
{ status: 502 }
131+
)
132+
}
133+
}
114134
return NextResponse.json({ success: true, output: { psp: mapPsp(data) } })
115135
}

apps/sim/blocks/blocks/uptimerobot.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,12 @@ export const UptimeRobotBlock: BlockConfig<UptimeRobotMonitorResponse> = {
123123
type: 'short-input',
124124
placeholder: 'e.g. https://example.com',
125125
condition: { field: 'operation', value: [...MONITOR_EDIT_OPS, 'list_monitors'] },
126-
required: { field: 'operation', value: 'create_monitor' },
126+
// Required to create a monitor, except Heartbeat monitors which have no URL.
127+
required: {
128+
field: 'operation',
129+
value: 'create_monitor',
130+
and: { field: 'type', value: 'HEARTBEAT', not: true },
131+
},
127132
},
128133

129134
// Monitor: interval (seconds) — aliased to `interval`

apps/sim/tools/uptimerobot/types.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import type { OutputProperty, ToolResponse } from '@/tools/types'
23

34
/** Base URL for the UptimeRobot v3 REST API. */
@@ -225,14 +226,18 @@ function parseNumberCsv(value: unknown): number[] | undefined {
225226
return numbers.length > 0 ? numbers : undefined
226227
}
227228

228-
/** Parses a JSON string (or passes through an already-parsed value). */
229-
function parseJson(value: unknown): unknown {
229+
/**
230+
* Parses a JSON string (or passes through an already-parsed value). Throws a
231+
* descriptive error on malformed JSON so a user-supplied value is never silently
232+
* dropped from the request body.
233+
*/
234+
function parseJson(value: unknown, field: string): unknown {
230235
if (value == null || value === '') return undefined
231236
if (typeof value !== 'string') return value
232237
try {
233238
return JSON.parse(value)
234-
} catch {
235-
return undefined
239+
} catch (error) {
240+
throw new Error(`Invalid JSON in "${field}": ${getErrorMessage(error, 'could not parse')}`)
236241
}
237242
}
238243

@@ -272,8 +277,12 @@ export function buildMonitorBody(
272277
assignDefined(body, 'groupId', params.groupId)
273278
assignDefined(body, 'successHttpResponseCodes', parseCsv(params.successHttpResponseCodes))
274279
assignDefined(body, 'tagNames', parseCsv(params.tagNames))
275-
assignDefined(body, 'assignedAlertContacts', parseJson(params.assignedAlertContacts))
276-
assignDefined(body, 'customHttpHeaders', parseJson(params.customHttpHeaders))
280+
assignDefined(
281+
body,
282+
'assignedAlertContacts',
283+
parseJson(params.assignedAlertContacts, 'assignedAlertContacts')
284+
)
285+
assignDefined(body, 'customHttpHeaders', parseJson(params.customHttpHeaders, 'customHttpHeaders'))
277286
return body
278287
}
279288

0 commit comments

Comments
 (0)