RSSHub-Gateway
One gateway, many feeds. RSSHub-Gateway is a production-ready reverse proxy for RSSHub and Upvote RSS that keeps feed URLs stable while scaling horizontally.
Why teams use it: - Keep a single stable feed URL across multiple upstreams - Route by prefix with per-group load balancing and health checks - Enforce gateway auth while staying RSSHub-compatible - Observe everything with metrics, pprof, and structured logs - Reload safely and cache responses in memory or Redis
Highlights
- Multi-backend routing:
/rsshub/for RSSHub,/upvote/for Upvote RSS - Prefix-based grouping with longest-match selection and per-group LB
- Short subscriptions:
/short/{name}with method301/302/proxy - Gateway auth:
?key=or?code=md5(path+key)+ RSSHub code injection - Health checks + passive eject + retry + fallback
- Prometheus metrics, pprof, JSON access/event logs
- SIGHUP config reload with rollback on failure
- Response cache (memory/redis) + auto reload polling
- Built-in docs:
/(README) and/wiki(Qoder RepoWiki)
Tip: in a deployed instance, open /wiki to read the full project guide.
Core Concepts
- Routes and groups: longest-prefix match, per-group LB, optional
strip_prefix - Upstreams: active health probes + passive eject ensure safe failover
- Auth: gateway key/code validation with RSSHub code injection only when needed
- Short links: internal or external targets with safe query passthrough
- Observability: metrics, pprof, and JSON access logs out of the box
Typical Use Cases
- One stable URL for multiple RSSHub clusters
- Fast failover across upstreams without changing feed URLs
- Short, memorable subscription links for feed readers
Architecture
Request flow stays simple: authenticate, route by prefix, pick upstream, proxy.
flowchart LR
Client -->|HTTP| Gateway
Gateway -->|/short/*| Short
Short -->|301/302| Redirect
Redirect --> Target
Short -->|proxy internal| Router
Short -->|proxy external| External
Gateway --> Router
Router --> Group
Group --> LB
LB --> RSSHub
LB --> Upvote
Gateway -->|/metrics| Prometheus
Gateway -->|logs| Logger
sequenceDiagram
participant C as Client
participant G as Gateway
participant R as Router
participant LB as Load Balancer
participant U as Upstream
participant E as External
C->>G: GET /short/fearnation?key=...
alt short redirect (external)
G->>G: Resolve short + strip key/code
G-->>C: 301 Location: https://fearnation.club/rss/?...
else short proxy (external)
G->>G: Resolve short + strip key/code
G->>E: Proxy request (UA + SNI)
E-->>G: Response
G-->>C: Response
else normal proxy
C->>G: GET /rsshub/path?key=...
G->>G: Validate key/code
G->>R: Select group by prefix
R-->>G: Group name
G->>LB: Pick upstream
LB-->>G: Upstream
G->>G: Remove key/code, inject upstream code (rsshub)
G->>U: Proxy request
U-->>G: Response
G-->>C: Response
end
Try It in 60 Seconds
Build locally and run with the sample config:
# build
make build
# run
./rsshub-gateway serve -c config.example.yaml
Docker
docker build -t rsshub-gateway:latest .
docker run --rm -p 8080:8080 rsshub-gateway:latest
Prebuilt images:
- docker pull nerdneils/rsshub-gateway:latest
- docker pull ghcr.io/nerdneilsfield/rsshub-gateway:latest
To use a custom config:
docker run --rm -p 8080:8080 \
-v "$(pwd)/config.example.yaml:/app/config.yaml:ro" \
rsshub-gateway:latest \
/app/rsshub-gateway serve -c /app/config.yaml
Deployment Example (Docker Compose)
services:
rsshub-gateway:
image: ghcr.io/nerdneilsfield/rsshub-gateway:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./config.yaml:/app/config.yaml:ro
command: ["/app/rsshub-gateway", "serve", "-c", "/app/config.yaml"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 30s
timeout: 5s
retries: 3
Configuration
The full schema lives in config.example.yaml.
How to write config (details)
- `routing.default_group` must match a group name. - Prefix rules use `allow`/`deny` with leading `/`; deny overrides allow. - `backend` must be `rsshub` or `upvote` (defaults to `rsshub`). - `strip_prefix` removes service prefixes like `/rsshub` or `/upvote` before proxying. - `gateway_auth` needs `access_key` and at least one of `accept_key`/`accept_code`. - `gateway_auth.bypass_paths` skips gateway auth for exact paths (still proxied/code-injected). - Upstream `access_key` is required for RSSHub code injection and healthcheck `?key=`. - Health check uses `path`, `interval_ms`, `timeout_ms`, `retries`. - `failover.passive_eject` requires `base_eject_ms <= max_eject_ms`. - Metrics require `metrics.accesskey` when enabled. - Pprof requires `pprof.accesskey` when enabled. - Cache requires provider (`memory` or `redis`) with TTL and size limits; caches GET 2xx/3xx only. - Auto reload polling uses config hash compare (`reload.auto.enabled` + `interval_ms`). - Short requires `short.path` (starts with `/`), unique entry names, and method `301`/`302`/`proxy`. External targets strip `key`/`code`.Full config example
```yaml server: listen: ":8080" timeout_ms: 8000 gateway_auth: enabled: true access_key: "ILoveRSSHub" accept_key: true accept_code: true bypass_paths: - "/favicon.ico" - "/logo.png" - "/robots.txt" - "/manifest.json" metrics: enabled: true path: "/metrics" accesskey: "PROM_KEY_123" pprof: enabled: false path: "/debug/pprof" accesskey: "PPROF_KEY_123" cache: enabled: false provider: "memory" ttl_ms: 3600000 max_item_bytes: 2097152 max_total_bytes: 52428800 redis: addr: "127.0.0.1:6379" password: "" db: 0 dial_timeout_ms: 1000 read_timeout_ms: 1000 write_timeout_ms: 1000 key_prefix: "rsshub_gateway" reload: auto: enabled: false interval_ms: 30000 short: enabled: true path: "/short" entries: - name: "latepost" target: "/rsshub/latepost/4" method: "301" - name: "reddit-top" target: "https://example.com/rss?platform=reddit" method: "302" routing: default_group: "rsshub-public" failover: retry: enabled: true max_retries: 1 passive_eject: enabled: true fail_threshold: 3 base_eject_ms: 10000 max_eject_ms: 60000 groups: - name: "rsshub-public" backend: "rsshub" strip_prefix: "/rsshub" priority: 10 allow: ["/rsshub/"] deny: [] lb: policy: "wrr" fallback_groups: ["rsshub-backup"] health: active: enabled: true path: "/healthz" interval_ms: 30000 timeout_ms: 10000 retries: 3 upstreams: - url: "http://rsshub-1:1200" weight: 3 access_key: "UP1KEY" - url: "http://rsshub-2:1200" weight: 2 access_key: "UP2KEY" - name: "rsshub-backup" backend: "rsshub" strip_prefix: "/rsshub" priority: 1 allow: ["/rsshub/"] deny: [] lb: policy: "hash" upstreams: - url: "http://rsshub-b1:1200" weight: 1 access_key: "B1KEY" - name: "upvote" backend: "upvote" strip_prefix: "/upvote" priority: 5 allow: ["/upvote/"] deny: [] lb: policy: "wrr" upstreams: - url: "http://upvote-rss:80" weight: 1 ```Authentication
Gateway access supports both key and code styles:
http://127.0.0.1:8080/rsshub/latepost/4?key=ACCESS_KEY
http://127.0.0.1:8080/rsshub/latepost/4?code=md5(path+ACCESS_KEY)
http://127.0.0.1:8080/upvote/?platform=reddit&key=ACCESS_KEY
Migration note (code auth):
If you previously used ?code= with /latepost/..., update the path to /rsshub/latepost/...
and compute md5("/rsshub/latepost/4"+ACCESS_KEY). Key-based access is unchanged.
Short Subscriptions
Short entries support method: 301|302|proxy. Redirects append the original query string;
external targets strip key/code to avoid leaking credentials. Proxy mode forwards the
request directly (internal targets keep key/code, external targets strip them).
GET /short/latepost?key=ACCESS_KEY
-> 301 Location: /rsshub/latepost/4?key=ACCESS_KEY
GET /short/reddit-top?sort=top&key=ACCESS_KEY
-> 302 Location: https://example.com/rss?platform=reddit&sort=top
GET /short/cdt?key=ACCESS_KEY
-> proxy https://chinadigitaltimes.net/chinese/feed
Upstream injection rules:
- Remove client key and code
- Inject code=md5(path+upstream_access_key) for RSSHub only
Routing and Load Balancing
- Match allow/deny by prefix, deny overrides allow
- Choose longest prefix, then higher priority, then config order
- Use
wrrorhashper group - Strip service prefix (like
/rsshubor/upvote) before proxying when configured
Health Checks
Active health checks call /healthz on each upstream. If the upstream requires
an ACCESS_KEY, the gateway automatically appends ?key=<upstream_access_key>.
If you use Docker Compose, update healthcheck like this:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:1200/healthz?key=${ACCESS_KEY}"]
Metrics
Metrics endpoint (requires access key):
GET /metrics?accesskey=<METRICS_ACCESS_KEY>
Metrics list
- rsshub_gateway_requests_total{method,group,route_prefix,status} - rsshub_gateway_request_duration_seconds_bucket{group,route_prefix} - rsshub_gateway_upstream_requests_total{group,upstream,status} - rsshub_gateway_upstream_health{group,upstream} - rsshub_gateway_upstream_eject_total{group,upstream} - rsshub_gateway_retry_total{group} - rsshub_gateway_fallback_total{from,to} - rsshub_gateway_config_reload_total{result}Pprof
Pprof endpoint (requires access key):
GET /debug/pprof/?accesskey=<PPROF_ACCESS_KEY>
Logging
Access logs are JSON per request. Event logs include health changes, ejections, and reload outcomes.
Suggested access log fields
- ts - level - req_id - method - path - group - upstream - route_prefix - status - duration_ms - retries - fallback_chain - err_type - errReload
Send SIGHUP to reload config without downtime:
kill -HUP <pid>
Development
make test
make cover
Release
goreleaser release --snapshot --clean --skip-publish
License
MIT