diff --git a/docs.json b/docs.json index 3f96b11..182309a 100644 --- a/docs.json +++ b/docs.json @@ -78,7 +78,7 @@ }, { "group": "Guides", - "pages": ["rest-api/guides/quickstart"] + "pages": ["rest-api/guides/quickstart", "rest-api/guides/proxy"] }, { "group": "Endpoints", diff --git a/rest-api/guides/proxy.mdx b/rest-api/guides/proxy.mdx new file mode 100644 index 0000000..25a8faf --- /dev/null +++ b/rest-api/guides/proxy.mdx @@ -0,0 +1,144 @@ +--- +title: "Proxy passthrough" +description: "Have the psxdata REST API fetch PSX data through your own HTTP or SOCKS proxy with the X-PSX-Proxy header." +--- + +Send an `X-PSX-Proxy` header with any PSX-backed request, and the API fetches that request's data from PSX **through your proxy** instead of its own connection. That includes the `X-Req-Id` request token PSX requires, which is fetched through the same proxy. Under the hood this uses the Python SDK's [proxy support](/sdk/guides/proxy). + +```bash +curl -H "X-PSX-Proxy: http://user:pass@proxy.example.com:8080" \ + https://psxdata-api.fastapicloud.dev/stocks/ENGRO/quote +``` + +Requests without the header behave exactly as before. Available from psxdata-api **0.4.0**. + + + Proxy passthrough is an opt-in server setting. If the server you're calling + hasn't enabled it, any request carrying `X-PSX-Proxy` returns + `400 bad_request` with the message `X-PSX-Proxy is not enabled on this server`. + + +--- + +## Cache first, proxy on a miss + +Proxied requests use the same cache as everyone else. Your proxy changes the route to PSX, not the data: + +1. **The data is already cached.** It's served straight from the cache. Your proxy is never looked up or contacted, and the request doesn't count toward the proxy limits below. +2. **The data isn't cached.** The API fetches it from PSX through your proxy and caches it, so later requests (yours or anyone else's) are served from the cache. + +The [caching rules](/rest-api/introduction#caching) are the same with or without a proxy. On `/stocks/{symbol}/historical`, `X-Cache` reports `HIT`, `MISS` or `STALE` as usual. + + + Because cached responses never touch your proxy, a `200` doesn't by itself + prove your proxy works. Use a request that misses the cache to test it. + + +PSX is HTTPS-only and the API verifies its certificates. A proxy only carries the encrypted connection, so it can't read or change the data it relays. + +--- + +## Supported proxies + +| Scheme | Example | +|---|---| +| `http://` | `http://proxy.example.com:8080` | +| `socks5://` | `socks5://proxy.example.com:1080` (the API resolves PSX's hostname) | +| `socks5h://` | `socks5h://proxy.example.com:1080` (your proxy resolves PSX's hostname) | + +- **Credentials** go in the URL: `http://user:pass@host:port`. Percent-encode special characters in the password (for example `@` becomes `%40`). +- **An explicit port is required.** It must be `80`, `443`, or between `1024` and `65535`. +- **`https://` proxies are not accepted.** The API pins each connection to the IP address it checked, which isn't compatible with verifying a TLS certificate for the proxy's hostname. +- **No path, query string or fragment.** At most 2048 characters. + +--- + +## What the API checks + +The API connects to an address you choose, so it checks your proxy before using it. + +**On every request** (no network involved): + +- Proxy passthrough must be enabled on the server. +- The URL must have a supported scheme and port, as above. + +**Only when PSX has to be fetched** (a cache miss): + +- **Public addresses only.** The proxy's hostname must resolve only to public internet addresses. Loopback, private ranges, link-local addresses (including cloud metadata such as `169.254.169.254`), carrier-grade NAT and multicast are all rejected, as are IPv6 forms that wrap any of those. +- **Pinned to the checked address.** The API connects to the exact IP it validated, so the hostname can't be switched to another address afterwards. +- **Reachability.** The proxy must accept a TCP connection within 5 seconds. +- **Limits.** At most 10 proxied PSX fetches per minute per IP address, and at most 4 in progress across the whole server. These are on top of the normal [60 requests per minute](/rest-api/introduction#rate-limits). + +--- + +## Errors + +All errors use the standard [error envelope](/rest-api/introduction#error-response). + +| Status | `error.code` | When | +|---|---|---| +| `400` | `bad_request` | Proxy passthrough is disabled on the server, the proxy URL is malformed or uses a disallowed scheme or port, or (on a cache miss) the host can't be resolved or resolves to a non-public address. | +| `429` | `rate_limited` | More than 10 proxied PSX fetches in a minute from your IP, or 4 proxied fetches already in progress on the server. Retry shortly. | +| `502` | `proxy_unreachable` | Your proxy didn't accept a connection within 5 seconds. | +| `503` | `psx_unavailable` | PSX was unreachable or failed through your proxy after retries. Same as without a proxy. | + +```json +{ + "error": { + "status": 502, + "code": "proxy_unreachable", + "message": "X-PSX-Proxy: proxy did not accept a connection" + } +} +``` + + + Error messages never include your proxy URL, and the API never logs proxy + credentials. Always send the proxy in the `X-PSX-Proxy` header, never in the + URL or query string. + + +--- + +## Examples + + + + ```python + import requests + + resp = requests.get( + "https://psxdata-api.fastapicloud.dev/stocks/ENGRO/historical", + params={"start": "2025-01-01", "end": "2025-01-31"}, + headers={"X-PSX-Proxy": "socks5h://user:pass@proxy.example.com:1080"}, + ) + resp.raise_for_status() + print(resp.headers.get("X-Cache"), len(resp.json()["data"])) + ``` + + + ```javascript + const resp = await fetch( + "https://psxdata-api.fastapicloud.dev/stocks/ENGRO/quote", + { headers: { "X-PSX-Proxy": "http://user:pass@proxy.example.com:8080" } } + ); + const body = await resp.json(); + if (!resp.ok) throw new Error(`${body.error.code}: ${body.error.message}`); + console.log(body.data); + ``` + + + + + Browsers send custom headers only after a CORS preflight. The API allows + any request header, so `X-PSX-Proxy` works from the browser too. Keep proxy + credentials out of frontend code, though: call the API from your backend if + your proxy needs a password. + + +--- + +## See also + +- [Python SDK proxy guide](/sdk/guides/proxy): use a proxy directly from Python, without the REST API. +- [Caching](/rest-api/introduction#caching): freshness rules and the `X-Cache` header. diff --git a/rest-api/introduction.mdx b/rest-api/introduction.mdx index 3c94cbc..4ece62a 100644 --- a/rest-api/introduction.mdx +++ b/rest-api/introduction.mdx @@ -62,6 +62,10 @@ Every `/historical` response tells you where it came from: `start` and `end` must be ISO dates in `YYYY-MM-DD` form, and `start` must not be after `end`. Anything else returns `422 bad_request`. +## Proxy passthrough + +Send an optional `X-PSX-Proxy` header (`http://`, `socks5://` or `socks5h://`, with an explicit port) to have the API fetch PSX data through your own proxy. Cached data is served without contacting your proxy; it's only used when PSX has to be fetched, and the result is cached for everyone. The server has to enable this feature. See the [proxy passthrough guide](/rest-api/guides/proxy) for supported URLs, the checks the API applies, and its limits. + ## Response format Every response — success or error — is a JSON object with a single top-level key. @@ -123,7 +127,8 @@ Every response — success or error — is a JSON object with a single top-level | `400` | `bad_request` | Invalid query parameters or request body. | | `404` | `not_found` | Symbol or resource not found. | | `422` | `bad_request` | Request validation failed (e.g. missing required field, wrong type, or a `start`/`end` date that is not `YYYY-MM-DD`). | -| `429` | `rate_limited` | 60 requests per minute per IP exceeded. | +| `429` | `rate_limited` | 60 requests per minute per IP exceeded, or a [proxy passthrough](/rest-api/guides/proxy) limit was hit. | +| `502` | `proxy_unreachable` | The proxy in your `X-PSX-Proxy` header did not accept a connection within 5 seconds. | | `503` | `psx_unavailable` | The PSX website was unreachable, or PSX is rate-limiting the API. When PSX is rate-limiting, the response includes a `Retry-After` header (seconds to wait). | | `500` | `internal_error` | Unexpected server error. |