Browse Source

Feature/add proxy route to FetchService (#7153)

* Add proxy route to FetchService

* Update changelog
pull/7164/head
Alexander Linder 2 days ago
committed by GitHub
parent
commit
80e81a36a9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 69
      apps/api/src/services/fetch/fetch.service.ts
  3. 19
      apps/api/src/services/fetch/interfaces/proxy-route.interface.ts
  4. 1
      libs/common/src/lib/config.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added support for routing outgoing requests through a per-domain proxy via the `PROXY_ROUTES` setting in the `FetchService`
## 3.18.0 - 2026-06-28
### Added

69
apps/api/src/services/fetch/fetch.service.ts

@ -4,6 +4,7 @@ import {
PROPERTY_API_KEY_OPENROUTER,
PROPERTY_OPENROUTER_MODEL,
PROPERTY_OPENROUTER_MODEL_WEB_FETCH,
PROPERTY_PROXY_ROUTES,
PROPERTY_WEB_FETCH_ROUTES
} from '@ghostfolio/common/config';
@ -12,6 +13,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText, jsonSchema, tool } from 'ai';
import ms from 'ms';
import { ProxyRoute } from './interfaces/proxy-route.interface';
import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
@Injectable()
@ -21,11 +23,17 @@ export class FetchService implements OnModuleInit {
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
private proxyRoutes: ProxyRoute[] = [];
private webFetchRoutes: WebFetchRoute[] = [];
public constructor(private readonly propertyService: PropertyService) {}
public async onModuleInit() {
this.proxyRoutes =
(await this.propertyService.getByKey<ProxyRoute[]>(
PROPERTY_PROXY_ROUTES
)) ?? [];
this.webFetchRoutes =
(await this.propertyService.getByKey<WebFetchRoute[]>(
PROPERTY_WEB_FETCH_ROUTES
@ -59,8 +67,10 @@ export class FetchService implements OnModuleInit {
}
}
const proxiedInput = this.applyProxyRoute(input);
try {
return await globalThis.fetch(input, init);
return await globalThis.fetch(proxiedInput, init);
} catch (error) {
if (error instanceof Error) {
this.logger.error(
@ -171,18 +181,73 @@ export class FetchService implements OnModuleInit {
}
}
/**
* Rewrites the origin (protocol, host and port) of a request when its domain
* matches a configured {@link ProxyRoute}, preserving path and query. Returns
* the input unchanged when no route matches or parsing fails.
*/
private applyProxyRoute(input: RequestInfo | URL): RequestInfo | URL {
let requestUrl: URL;
try {
requestUrl = new URL(
input instanceof Request ? input.url : input.toString()
);
} catch {
return input;
}
const route = this.proxyRoutes.find(({ domain }) => {
return this.hostnameMatchesDomain({
domain,
hostname: requestUrl.hostname
});
});
if (!route) {
return input;
}
try {
const proxyUrl = new URL(route.url);
requestUrl.host = proxyUrl.host;
requestUrl.protocol = proxyUrl.protocol;
} catch {
this.logger.warn(
`Skipping proxy route for "${route.domain}": invalid url "${route.url}"`
);
return input;
}
return input instanceof Request
? new Request(requestUrl.toString(), input)
: requestUrl.toString();
}
private getMatchingWebFetchRoute(url: string) {
try {
const { hostname } = new URL(url);
return this.webFetchRoutes.find(({ domain }) => {
return hostname === domain || hostname.endsWith(`.${domain}`);
return this.hostnameMatchesDomain({ domain, hostname });
});
} catch {
return undefined;
}
}
private hostnameMatchesDomain({
domain,
hostname
}: {
domain: string;
hostname: string;
}): boolean {
return hostname === domain || hostname.endsWith(`.${domain}`);
}
private redactUrl(rawUrl: string): string {
try {
const url = new URL(rawUrl);

19
apps/api/src/services/fetch/interfaces/proxy-route.interface.ts

@ -0,0 +1,19 @@
/**
* Overrides the origin (protocol, host and port) of outgoing requests for a
* given domain.
*
* Configured via the `PROXY_ROUTES` property as a JSON array, e.g.
*
* [
* {
* "domain": "example.com",
* "url": "http://example-proxy:8191"
* }
* ]
*
* Matches the domain itself and its subdomains (e.g. `api.example.com`).
*/
export interface ProxyRoute {
domain: string;
url: string;
}

1
libs/common/src/lib/config.ts

@ -259,6 +259,7 @@ export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES';
export const PROPERTY_REFERRAL_PARTNERS = 'REFERRAL_PARTNERS';
export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';

Loading…
Cancel
Save