Skip to content

Commit dc9ca20

Browse files
committed
feat(graphql-api): allow whitelisted IPs to bypass rate limits
1 parent 5f2f8f8 commit dc9ca20

File tree

4 files changed

+78
-13
lines changed

4 files changed

+78
-13
lines changed

graphql-api/README.md

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,45 @@ query getVariant($variantId: String!) {
7171
}
7272
`
7373

74-
fetch("https://gnomad.broadinstitute.org/api", {
75-
method: "POST",
76-
body: JSON.stringify({
77-
query: QUERY,
78-
variables: {
79-
variantId: "1-55516888-G-GA",
80-
},
81-
}),
82-
headers: {
83-
"Content-Type": "application/json",
84-
},
74+
fetch('https://gnomad.broadinstitute.org/api', {
75+
method: 'POST',
76+
body: JSON.stringify({
77+
query: QUERY,
78+
variables: {
79+
variantId: '1-55516888-G-GA',
80+
},
81+
}),
82+
headers: {
83+
'Content-Type': 'application/json',
84+
},
8585
})
86-
.then(response => response.json())
87-
.then(data => console.log(data.data))
86+
.then((response) => response.json())
87+
.then((data) => console.log(data.data))
88+
```
89+
90+
## Rate Limiting
91+
92+
The GraphQL API includes rate limiting at the IP level, defined in `rate-limiting.ts`.
93+
94+
Certain IPs are whitelisted, allowing them to bypass the rate limits imposed by the API. These are defined in the file `gs://gnomad-browser/whitelist.json`. This json file's format is:
95+
96+
```
97+
{
98+
"whitelisted_ips": [
99+
{
100+
"ip": "123.456.78.90",
101+
"description": "Example 1",
102+
"reason": "Lorem ipsum",
103+
"date_added": "2025-09-30"
104+
},
105+
{
106+
"ip": "234.567.89.0",
107+
"description": "Example 2",
108+
"reason": "Lorem ipsum",
109+
"date_added": "YYYY-MM-DD"
110+
}
111+
]
112+
}
88113
```
114+
115+
To whitelist additional IPs, add another entry to the `whitelisted_ips` array.

graphql-api/src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { client as esClient } from './elasticsearch'
88
import graphQLApi from './graphql/graphql-api'
99
import logger from './logger'
1010

11+
import { loadWhitelist } from './whitelist'
12+
1113
process.on('uncaughtException', (error) => {
1214
logger.error(error)
1315
process.exit(1)
@@ -75,6 +77,8 @@ app.use(function requestLogMiddleware(request: any, response: any, next: any) {
7577
next()
7678
})
7779

80+
loadWhitelist()
81+
7882
const context = { esClient }
7983

8084
app.use('/api/', graphQLApi({ context }))

graphql-api/src/graphql/rate-limiting.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Redis } from 'ioredis'
22
import config from '../config'
33
import { UserVisibleError } from '../errors'
44
import logger from '../logger'
5+
import { isWhitelistedIP } from '../whitelist'
56

67
let rateLimitDb: Redis
78

@@ -47,6 +48,10 @@ export const applyRateLimits = async (request: any) => {
4748

4849
const clientId = request.ip
4950

51+
if (isWhitelistedIP(clientId)) {
52+
return
53+
}
54+
5055
try {
5156
const totalRequestsInWindow = await increaseRateLimitCounter(
5257
`rate_limit:1m:requests:${clientId}:${rateLimitWindow}`,

graphql-api/src/whitelist.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Storage } from '@google-cloud/storage'
2+
import logger from './logger'
3+
4+
const storage = new Storage()
5+
let whitelistedIPs: Set<string> = new Set()
6+
7+
type WhitelistEntry = {
8+
ip: string
9+
description: string
10+
date_added: string
11+
reason_added: string
12+
}
13+
14+
export async function loadWhitelist() {
15+
try {
16+
const bucket = storage.bucket('gnomad-browser')
17+
const file = bucket.file('whitelist.json')
18+
const [contents] = await file.download()
19+
const data = JSON.parse(contents.toString())
20+
whitelistedIPs = new Set(data.whitelisted_ips.map((entry: WhitelistEntry) => entry.ip))
21+
logger.info(`Loaded ${whitelistedIPs.size} IPs from the whitelist.`)
22+
} catch (err) {
23+
logger.error(`Failed to load whitelist: ${err}.`)
24+
}
25+
}
26+
27+
export function isWhitelistedIP(ip: string): boolean {
28+
return whitelistedIPs.has(ip)
29+
}

0 commit comments

Comments
 (0)