{"record":{"id":"54e93888f3de8ea4","repo":"Budibase/budibase","slug":"rest-query-path-must-remain-on-the-datasource-orig","errorCode":null,"errorMessage":"REST query path must remain on the datasource origin","messagePattern":"REST query path must remain on the datasource origin","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/server/src/integrations/rest.ts","lineNumber":717,"sourceCode":"    }\n  }\n\n  private assertSameOrigin(url: string, rawPath: string | undefined) {\n    const finalOrigin = this.getOrigin(url)\n\n    const expectedOriginUrls: string[] = []\n    if (this.config.url) {\n      expectedOriginUrls.push(this.getUrl())\n    }\n    if (rawPath !== undefined) {\n      expectedOriginUrls.push(this.getUrl(rawPath))\n    }\n\n    const isCrossOrigin = expectedOriginUrls.some(\n      expectedUrl => this.getOrigin(expectedUrl) !== finalOrigin\n    )\n    if (isCrossOrigin) {\n      throw new Error(\"REST query path must remain on the datasource origin\")\n    }\n  }\n\n  private mergedQueryParams(fields: RestQuery, config: RestPreviewConfig) {\n    const queryParams = fields.queryString ? qs.decode(fields.queryString) : {}\n    return { ...(config.defaultQueryParameters || {}), ...queryParams }\n  }\n\n  private composeUrl(fields: RestQuery, config: RestPreviewConfig): string {\n    const { path = \"\", queryString = \"\", pagination, paginationValues } = fields\n    const defaultQueryParameters = config.defaultQueryParameters || {}\n    let mergedQueryString = queryString\n    if (Object.keys(defaultQueryParameters).length > 0) {\n      mergedQueryString = qs.encode(this.mergedQueryParams(fields, config))\n    }\n\n    return this.getUrl(\n      path,","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/integrations/rest.ts#L699-L735","documentation":"assertSameOrigin enforces that the final request URL of a REST query stays on the origin of the datasource configuration. It compares the origin of the composed URL against the origins derived from this.config.url and any rawPath; if any differ, it throws 'REST query path must remain on the datasource origin'. This prevents queries from being redirected to arbitrary hosts - an SSRF/open-redirect protection for stored REST datasources.","triggerScenarios":"Calling buildRequest where the fields.path (or pagination next-URL) resolves to a different scheme/host/port than the datasource's configured url - e.g. path is an absolute URL to another host, or a pagination 'next' link returned by the API points to a different origin than the datasource base URL.","commonSituations":"API's pagination response includes absolute 'next' URLs on a CDN or different subdomain than the configured datasource base URL; query path built with an absolute URL (https://other-api.com/...) instead of a relative path; http vs https mismatch between config url and path; port mismatch (localhost:3000 vs localhost:4001) in local dev.","solutions":["Make the query path relative and same-origin: use '/resource' instead of an absolute 'https://other-host/resource'","If pagination returns absolute URLs on a different origin, use relative pagination parameters (offset/cursor fields) instead of following the next-URL directly","Align scheme and host: set the datasource url to the actual API origin (https and correct host/port) used by the path","If the API genuinely serves data across subdomains, create a separate REST datasource per origin","Strip absolute next-links from pagination config so composed URLs derive only from the datasource base"],"exampleFix":"// before\n{ path: \"https://cdn.example.com/v2/users\" } // config url: https://api.example.com\n// after\n{ path: \"/v2/users\" }","handlingStrategy":"validation","validationCode":"function isSameOrigin(base, target) {\n  const b = new URL(base), t = new URL(target, base)\n  return b.origin === t.origin\n}\nif (!isSameOrigin(datasourceUrl, pathOrNextUrl)) {\n  throw new Error(\"path must stay on the datasource origin\")\n}","typeGuard":"function isRelativePath(p) {\n  return typeof p === \"string\" && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p)\n}","tryCatchPattern":"try {\n  const result = await restQuery.execute()\n} catch (e) {\n  if (e.message === \"REST query path must remain on the datasource origin\") {\n    // rewrite path to relative, or configure a separate datasource for that origin\n  } else { throw e }\n}","preventionTips":["Always use relative paths in REST query paths; never absolute URLs to other hosts","Prefer relative/cursor pagination parameters over following absolute next-URLs returned by APIs","Match scheme, host and port between the datasource url and every path used","Create one datasource per origin instead of cross-origin paths"],"tags":["rest","security","ssrf","url"],"backgroundTag":"cross-origin-request-blocked","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}