|
| 1 | +# Remote image URLs |
| 2 | + |
| 3 | +`upload_image` accepts exactly one of `image_path`, `image_base64` or |
| 4 | +`image_url`. With `image_url`, the server downloads the image and then uploads |
| 5 | +it through the same Substack endpoint as the other two. The result is the same: |
| 6 | +a hosted image URL that anyone with the link can fetch. |
| 7 | + |
| 8 | +```json |
| 9 | +{ "image_url": "https://images.example.com/chart.png" } |
| 10 | +``` |
| 11 | + |
| 12 | +## What is refused |
| 13 | + |
| 14 | +The download is a plain request made by the server, separate from the Substack |
| 15 | +client. It sends no Substack cookies, session token, `Authorization` or |
| 16 | +`Referer` header. |
| 17 | + |
| 18 | +| Rule | Error `code` | |
| 19 | +| --- | --- | |
| 20 | +| Not an absolute `https` URL, contains credentials, or uses a non-default port | `invalid_url` | |
| 21 | +| Resolves to or names a loopback, private, carrier-grade NAT, link-local (including `169.254.169.254`), multicast, documentation or other reserved address | `blocked_destination` | |
| 22 | +| Host does not resolve | `dns_failed` | |
| 23 | +| More than 3 redirects, or a redirect without `Location` | `too_many_redirects`, `http_status` | |
| 24 | +| Any status other than 200 | `http_status` | |
| 25 | +| Compressed or otherwise encoded body | `unsupported_encoding` | |
| 26 | +| Declared or streamed size above 5 MB | `too_large` | |
| 27 | +| Whole download, including redirects, takes longer than 15 seconds | `timeout` | |
| 28 | +| Bytes are not PNG, JPEG, GIF, WebP or AVIF (SVG and HEIC included) | `unsupported_type` | |
| 29 | +| `Content-Type` missing or different from the detected format | `type_mismatch` | |
| 30 | +| Connection failed or ended early | `network` | |
| 31 | + |
| 32 | +A refused download returns `isError: true` with |
| 33 | +`{ "code", "message", "upload_attempts": 0 }`, and nothing is uploaded. |
| 34 | + |
| 35 | +## Address checks |
| 36 | + |
| 37 | +IPv4 addresses must be outside the special-purpose ranges listed above. IPv6 |
| 38 | +addresses must be global unicast (`2000::/3`). The server also refuses 6to4, |
| 39 | +Teredo, NAT64 and documentation prefixes, which can embed IPv4 destinations. |
| 40 | +IPv4-mapped forms such as `::ffff:169.254.169.254`, unique-local addresses such |
| 41 | +as `fd00:ec2::254`, and addresses with a zone ID are refused. |
| 42 | + |
| 43 | +The check happens when each connection is made, including after every |
| 44 | +redirect. The server does not look the host up in advance and connect later, so |
| 45 | +DNS rebinding can't swap in a private address between check and connection. If |
| 46 | +a host resolves to several addresses and any one is refused, the whole request |
| 47 | +is refused. Literal IP addresses in a URL or `Location` header are checked |
| 48 | +before connecting. |
| 49 | + |
| 50 | +Proxy environment variables are not used for this download. |
| 51 | + |
| 52 | +## Deployments |
| 53 | + |
| 54 | +The stdio and self-hosted HTTP servers include remote downloads. The Cloudflare |
| 55 | +Worker shares the tool list but not the Node network stack these checks depend |
| 56 | +on. There, `image_url` returns `remote_image_unavailable`; use `image_path` or |
| 57 | +`image_base64` instead. |
| 58 | + |
| 59 | +Local file and data URI uploads are unchanged: `image_path` still infers the |
| 60 | +type from the file extension. The byte-signature and address checks above apply |
| 61 | +only to `image_url`. |
0 commit comments