{"record":{"id":"1baf718c1477f3d8","repo":"ComposioHQ/composio","slug":"refusing-to-fetch-a-non-http-s-url-scheme-url","errorCode":null,"errorMessage":"Refusing to fetch a non-http(s) URL (scheme \"${url.protocol}\")","messagePattern":"Refusing to fetch a non-http\\(s\\) URL \\(scheme \"(.+?)\"\\)","errorType":"exception","errorClass":"ComposioBlockedInternalUrlError","httpStatus":null,"severity":"error","filePath":"ts/packages/core/src/utils/ssrfGuard.node.ts","lineNumber":180,"sourceCode":"/**\n * Validate a single URL: it must be http(s), its host must resolve, and every\n * resolved address must be publicly routable. Throws\n * {@link ComposioBlockedInternalUrlError} otherwise.\n *\n * @returns the validated addresses to connect to, in resolver order. Callers\n * must connect to *those* rather than let the client resolve the hostname\n * again; see {@link ssrfSafeFetch}.\n */\nexport const assertSafeFetchTarget = async (rawUrl: string): Promise<string[]> => {\n  let url: URL;\n  try {\n    url = new URL(rawUrl);\n  } catch {\n    throw new ComposioBlockedInternalUrlError('Refusing to fetch a malformed URL', { url: rawUrl });\n  }\n\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new ComposioBlockedInternalUrlError(\n      `Refusing to fetch a non-http(s) URL (scheme \"${url.protocol}\")`,\n      { url: rawUrl }\n    );\n  }\n\n  const host = url.hostname.replace(/^\\[|\\]$/g, '');\n\n  let resolved: Array<{ address: string }>;\n  try {\n    resolved = await lookup(host, { all: true, verbatim: true });\n  } catch {\n    throw new ComposioBlockedInternalUrlError(`Could not resolve host \"${host}\"`, { url: rawUrl });\n  }\n\n  if (resolved.length === 0) {\n    throw new ComposioBlockedInternalUrlError(`Could not resolve host \"${host}\"`, { url: rawUrl });\n  }\n","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/ts/packages/core/src/utils/ssrfGuard.node.ts#L162-L198","documentation":"The SSRF guard only fetches http: and https: URLs. Any other scheme (file:, ftp:, data:, gopher:, etc.) is rejected before any connection is made, since non-HTTP transports can bypass network policy.","triggerScenarios":"Passing a URL with a scheme other than http/https to a URL-upload API — e.g. 'file:///etc/passwd', 'ftp://host/file', or a data: URI.","commonSituations":"LLM or user input supplying file:// paths expecting local upload semantics; legacy ftp endpoints; accidentally prefixing data URIs.","solutions":["Use the local-file upload API for files on disk instead of a file:// URL","Convert ftp/data sources to an https endpoint first, or download and upload bytes directly","Validate url.protocol === 'https:' client-side before calling"],"exampleFix":"// before\nawait upload.uploadFileAtUrl('file:///etc/hosts');\n\n// after\n// upload local files via the disk-based API\nawait upload.readFileFromDisk('/path/to/file');","handlingStrategy":"type-guard","validationCode":"if (!/^https?:\\/\\//i.test(url)) throw new Error('Only http(s) URLs are supported');","typeGuard":"const isHttpUrl = (u: string): boolean => { try { const p = new URL(u).protocol; return p === 'http:' || p === 'https:'; } catch { return false; } };","tryCatchPattern":"try {\n  await upload.uploadFileAtUrl(url);\n} catch (e) {\n  if (e instanceof ComposioBlockedInternalUrlError && e.message.includes('non-http(s)')) {\n    // use the local-file upload API for file:// targets\n  }\n}","preventionTips":["Validate scheme client-side before any URL upload","Use the disk-based API for local files instead of file:// URLs","Whitelist https only in your input validation"],"tags":["ssrf","url-validation","security","file-upload"],"backgroundTag":"unsupported-url-scheme","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}