@emircansahin/ghostfetch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -0
- package/dist/cjs/classifier.js +118 -0
- package/dist/cjs/classifier.js.map +1 -0
- package/dist/cjs/client.js +411 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/errors.js +43 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/proxy-manager.js +172 -0
- package/dist/cjs/proxy-manager.js.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/classifier.js +111 -0
- package/dist/esm/client.js +403 -0
- package/dist/esm/errors.js +35 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/proxy-manager.js +167 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/classifier.d.ts +36 -0
- package/dist/types/classifier.d.ts.map +1 -0
- package/dist/types/client.d.ts +68 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/proxy-manager.d.ts +55 -0
- package/dist/types/proxy-manager.d.ts.map +1 -0
- package/dist/types/types.d.ts +141 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ghostfetch
|
|
2
|
+
|
|
3
|
+
Resilient HTTP client for Node.js with CycleTLS, automatic proxy rotation, smart error classification, and per-site custom interceptors.
|
|
4
|
+
|
|
5
|
+
Built for backend developers who need to fetch data from sites that aggressively block automated requests.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **CycleTLS** — TLS fingerprint spoofing (bypasses Cloudflare and similar WAFs)
|
|
10
|
+
- **Proxy rotation** — random proxy selection per request with automatic health check
|
|
11
|
+
- **Smart error classification** — proxy vs server vs ambiguous errors
|
|
12
|
+
- **Proxy banning** — auto-ban failing proxies with configurable TTL
|
|
13
|
+
- **Custom interceptors** — per-site response handling (`retry`, `ban`, `skip`)
|
|
14
|
+
- **Default status handling** — 429/503 auto-retry, 407 proxy ban
|
|
15
|
+
- **Cloudflare detection** — JS challenge detection with descriptive errors
|
|
16
|
+
- **Country-based proxy selection** — auto-resolved via ipinfo.io
|
|
17
|
+
- **forceProxy mode** — wait for available proxy instead of proceeding without one
|
|
18
|
+
- **Health check on init** — dead proxies are discarded before any request
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install ghostfetch
|
|
24
|
+
# or
|
|
25
|
+
pnpm add ghostfetch
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { GhostFetch } from 'ghostfetch';
|
|
32
|
+
|
|
33
|
+
const client = new GhostFetch({
|
|
34
|
+
proxies: [
|
|
35
|
+
'http://user:pass@host:8001',
|
|
36
|
+
'http://user:pass@host:8002',
|
|
37
|
+
],
|
|
38
|
+
timeout: 30000,
|
|
39
|
+
retry: { delays: [5000, 15000, 30000] }, // 3 retries: wait 5s, 15s, 30s
|
|
40
|
+
ban: { maxFailures: 3, duration: 60 * 60 * 1000 },
|
|
41
|
+
// ban: false — disable proxy banning entirely
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Wait for health check to complete
|
|
45
|
+
const health = await client.ready();
|
|
46
|
+
console.log(health);
|
|
47
|
+
// { total: 2, healthy: 2, dead: 0, countries: { US: 1, DE: 1 }, proxies: { ... } }
|
|
48
|
+
|
|
49
|
+
// Make requests
|
|
50
|
+
const res = await client.get('https://api.example.com/data');
|
|
51
|
+
console.log(res.status, res.body);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Custom Interceptors
|
|
55
|
+
|
|
56
|
+
Interceptors let you define per-site response handling. You can add them at the instance level (applies to all matching requests) or at the request level (applies to that single request only).
|
|
57
|
+
|
|
58
|
+
### Instance-level interceptor
|
|
59
|
+
|
|
60
|
+
Matches requests by URL. First matching interceptor takes full ownership — default status handling (429, 503, etc.) is bypassed.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
client.addInterceptor({
|
|
64
|
+
name: 'example-api',
|
|
65
|
+
match: (url) => url.includes('example.com'),
|
|
66
|
+
check: (res) => {
|
|
67
|
+
if (res.status === 401) return 'skip'; // don't retry auth errors
|
|
68
|
+
if (res.body.includes('rate limit')) return 'retry'; // retry with different proxy
|
|
69
|
+
if (res.body.includes('blocked')) return 'ban'; // ban this proxy + retry
|
|
70
|
+
return null; // use default behavior
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Request-level interceptor
|
|
76
|
+
|
|
77
|
+
No `match` needed — it applies to this specific request. Takes priority over instance-level interceptors.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const res = await client.get('https://special-api.com/data', {
|
|
81
|
+
interceptor: {
|
|
82
|
+
check: (res) => {
|
|
83
|
+
if (res.status === 401) return 'skip';
|
|
84
|
+
if (res.status === 200 && res.body.includes('error')) return 'retry';
|
|
85
|
+
return null;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Interceptor priority
|
|
92
|
+
|
|
93
|
+
1. **Request-level interceptor** — checked first, if it returns non-null action, it wins
|
|
94
|
+
2. **Instance-level interceptors** — checked next, first `match` takes ownership
|
|
95
|
+
3. **Default status handling** — only runs if no interceptor claimed the response
|
|
96
|
+
|
|
97
|
+
### Actions
|
|
98
|
+
|
|
99
|
+
| Action | Proxy effect | Retry | Default bypass |
|
|
100
|
+
|--------|-------------|-------|----------------|
|
|
101
|
+
| `'retry'` | not penalized | yes | yes |
|
|
102
|
+
| `'ban'` | fail count +1 | yes | yes |
|
|
103
|
+
| `'skip'` | not penalized | no, return response | yes |
|
|
104
|
+
| `null` | — | — | no, defaults apply |
|
|
105
|
+
|
|
106
|
+
## Country-Based Proxy Selection
|
|
107
|
+
|
|
108
|
+
All proxies are automatically resolved via ipinfo.io on init. This gives you country-level control over which proxy handles which request — useful when certain APIs only accept traffic from specific regions.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Only use a German proxy for this request
|
|
112
|
+
const res = await client.get('https://eu-only-api.com/data', {
|
|
113
|
+
country: 'DE',
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Force Proxy Mode
|
|
118
|
+
|
|
119
|
+
By default, if all proxies are banned or unavailable, requests proceed without a proxy. Enable `forceProxy` to make the request wait until a proxy becomes available (ban expires or proxy list is refreshed). This is useful for cron jobs where requests without a proxy are pointless.
|
|
120
|
+
|
|
121
|
+
**What happens when `forceProxy: true` and no proxy is available?**
|
|
122
|
+
|
|
123
|
+
| Scenario | Behavior |
|
|
124
|
+
|----------|----------|
|
|
125
|
+
| Proxy list is empty (none configured) | Throws `NoProxyAvailableError` immediately — nothing to wait for |
|
|
126
|
+
| Proxies exist but all banned | Waits until a ban expires or `onProxyRefresh` provides fresh proxies |
|
|
127
|
+
|
|
128
|
+
You can set it as the instance default or override per-request:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// Instance default: all requests wait for proxy
|
|
132
|
+
const client = new GhostFetch({
|
|
133
|
+
proxies: [...],
|
|
134
|
+
forceProxy: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Override per-request: this specific endpoint works without proxy
|
|
138
|
+
const publicData = await client.get('https://public-api.com/data', {
|
|
139
|
+
forceProxy: false,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Or the other way: instance default is false, but this request needs a proxy
|
|
143
|
+
const protectedData = await client.get('https://protected-api.com/data', {
|
|
144
|
+
forceProxy: true,
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Disable Proxy Banning
|
|
149
|
+
|
|
150
|
+
If your proxy provider handles rotation internally (e.g. BrightData, Oxylabs residential) or you have a small proxy pool you don't want to lose, you can disable banning entirely:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const client = new GhostFetch({
|
|
154
|
+
proxies: [...],
|
|
155
|
+
ban: false, // no proxy will ever be banned
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
With `ban: false`, failed proxies are never penalized — they stay in rotation regardless of errors. Retry still works, just without proxy exclusion logic.
|
|
160
|
+
|
|
161
|
+
## Proxy Refresh
|
|
162
|
+
|
|
163
|
+
Provide an `onProxyRefresh` callback to fetch a fresh proxy list from your provider. When triggered, all existing bans are cleared and the new proxies go through health check before entering the pool.
|
|
164
|
+
|
|
165
|
+
| Config | Behavior |
|
|
166
|
+
|--------|----------|
|
|
167
|
+
| `onProxyRefresh` only | No automatic refresh — call `client.refreshProxies()` manually |
|
|
168
|
+
| `onProxyRefresh` + `proxyRefreshInterval` | Auto-refresh at the given interval |
|
|
169
|
+
| Neither | No refresh capability — initial proxy list is used for the lifetime |
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Auto-refresh every hour
|
|
173
|
+
const client = new GhostFetch({
|
|
174
|
+
proxies: [...],
|
|
175
|
+
proxyRefreshInterval: 60 * 60 * 1000,
|
|
176
|
+
onProxyRefresh: async () => {
|
|
177
|
+
return ['http://user:pass@newhost:8001', ...];
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Or manual-only: no interval, call when you need it
|
|
182
|
+
const client2 = new GhostFetch({
|
|
183
|
+
proxies: [...],
|
|
184
|
+
onProxyRefresh: async () => fetchFromProvider(),
|
|
185
|
+
});
|
|
186
|
+
await client2.refreshProxies(); // triggers onProxyRefresh → health check → pool updated
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
|
|
191
|
+
ghostfetch throws specific error classes so you can handle each scenario precisely:
|
|
192
|
+
|
|
193
|
+
- **`CloudflareJSChallengeError`** — the target site requires a browser-level JS challenge that CycleTLS can't solve. You'll need puppeteer-extra with stealth plugin for this.
|
|
194
|
+
- **`NoProxyAvailableError`** — all proxies are banned and `forceProxy` is not enabled, or the proxy list is empty.
|
|
195
|
+
- **`MaxRetriesExceededError`** — all retry attempts failed. Contains `.attempts` count and `.lastError` with the final error details.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import {
|
|
199
|
+
CloudflareJSChallengeError,
|
|
200
|
+
NoProxyAvailableError,
|
|
201
|
+
MaxRetriesExceededError,
|
|
202
|
+
} from 'ghostfetch';
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const res = await client.get('https://example.com');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err instanceof CloudflareJSChallengeError) {
|
|
208
|
+
// Needs headless browser (puppeteer-extra with stealth plugin)
|
|
209
|
+
}
|
|
210
|
+
if (err instanceof NoProxyAvailableError) {
|
|
211
|
+
// All proxies banned or list empty
|
|
212
|
+
}
|
|
213
|
+
if (err instanceof MaxRetriesExceededError) {
|
|
214
|
+
console.log(err.attempts, err.lastError);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyError = classifyError;
|
|
4
|
+
exports.isCloudflareChallenge = isCloudflareChallenge;
|
|
5
|
+
exports.checkInterceptors = checkInterceptors;
|
|
6
|
+
exports.checkDefaultRetryStatus = checkDefaultRetryStatus;
|
|
7
|
+
/** Error codes that are definitely proxy/network failures — request never reached the server. */
|
|
8
|
+
const PROXY_ERROR_CODES = new Set([
|
|
9
|
+
'ECONNREFUSED',
|
|
10
|
+
'ENOTFOUND',
|
|
11
|
+
'EAI_AGAIN',
|
|
12
|
+
'EHOSTUNREACH',
|
|
13
|
+
'ENETUNREACH',
|
|
14
|
+
'EPIPE',
|
|
15
|
+
]);
|
|
16
|
+
/** Error codes that could be proxy OR server — we can't tell for sure. */
|
|
17
|
+
const AMBIGUOUS_ERROR_CODES = new Set([
|
|
18
|
+
'ETIMEDOUT',
|
|
19
|
+
'ECONNRESET',
|
|
20
|
+
'ECONNABORTED',
|
|
21
|
+
]);
|
|
22
|
+
const PROXY_ERROR_KEYWORDS = [
|
|
23
|
+
'proxy',
|
|
24
|
+
'tunnel',
|
|
25
|
+
'connect econnrefused',
|
|
26
|
+
];
|
|
27
|
+
const AMBIGUOUS_ERROR_KEYWORDS = [
|
|
28
|
+
'socket hang up',
|
|
29
|
+
'timeout',
|
|
30
|
+
];
|
|
31
|
+
/** Cloudflare JS challenge detection patterns */
|
|
32
|
+
const CF_CHALLENGE_PATTERNS = [
|
|
33
|
+
'cf-browser-verification',
|
|
34
|
+
'cf_chl_opt',
|
|
35
|
+
'jschl_vc',
|
|
36
|
+
'jschl_answer',
|
|
37
|
+
'Checking your browser',
|
|
38
|
+
'Just a moment...',
|
|
39
|
+
'_cf_chl_tk',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Default status codes that should trigger retry.
|
|
43
|
+
* - 'server': retry with different proxy, proxy is not penalized
|
|
44
|
+
* - 'proxy': retry with different proxy, proxy fail count incremented
|
|
45
|
+
*/
|
|
46
|
+
const DEFAULT_RETRY_STATUSES = {
|
|
47
|
+
429: 'server', // Rate limit — not proxy's fault, just retry with different IP
|
|
48
|
+
503: 'server', // Service unavailable — server overloaded
|
|
49
|
+
407: 'proxy', // Proxy authentication required — proxy is broken
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Classify an error as proxy, server, or ambiguous.
|
|
53
|
+
*
|
|
54
|
+
* - proxy: request definitely never reached the server (DNS fail, connection refused, etc.)
|
|
55
|
+
* - server: an HTTP response was received — the proxy worked fine
|
|
56
|
+
* - ambiguous: could be either (timeout, connection reset) — proxy should NOT be penalized
|
|
57
|
+
*/
|
|
58
|
+
function classifyError(error) {
|
|
59
|
+
if (error && typeof error === 'object') {
|
|
60
|
+
const err = error;
|
|
61
|
+
// If there's an HTTP status code, the request reached the server → server error
|
|
62
|
+
if (err.status && typeof err.status === 'number') {
|
|
63
|
+
return 'server';
|
|
64
|
+
}
|
|
65
|
+
const code = (err.code || err.errno);
|
|
66
|
+
const message = (err.message || '').toLowerCase();
|
|
67
|
+
// Check definite proxy errors first
|
|
68
|
+
if (code && PROXY_ERROR_CODES.has(code)) {
|
|
69
|
+
return 'proxy';
|
|
70
|
+
}
|
|
71
|
+
if (PROXY_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
|
|
72
|
+
return 'proxy';
|
|
73
|
+
}
|
|
74
|
+
// Check ambiguous errors
|
|
75
|
+
if (code && AMBIGUOUS_ERROR_CODES.has(code)) {
|
|
76
|
+
return 'ambiguous';
|
|
77
|
+
}
|
|
78
|
+
if (AMBIGUOUS_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
|
|
79
|
+
return 'ambiguous';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Default: treat unknown errors as server errors (keep proxies alive)
|
|
83
|
+
return 'server';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a successful response is actually a Cloudflare JS challenge.
|
|
87
|
+
*/
|
|
88
|
+
function isCloudflareChallenge(response) {
|
|
89
|
+
if (response.status === 403 || response.status === 503) {
|
|
90
|
+
return CF_CHALLENGE_PATTERNS.some((pattern) => response.body.includes(pattern));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run interceptors against a response.
|
|
96
|
+
*
|
|
97
|
+
* First interceptor whose `match` returns true takes ownership.
|
|
98
|
+
* Its `check` result determines the action. Default status handling
|
|
99
|
+
* is bypassed whenever an interceptor matches (even if check returns null).
|
|
100
|
+
*/
|
|
101
|
+
function checkInterceptors(url, response, interceptors) {
|
|
102
|
+
for (const interceptor of interceptors) {
|
|
103
|
+
if (!interceptor.match(url))
|
|
104
|
+
continue;
|
|
105
|
+
const action = interceptor.check(response);
|
|
106
|
+
return { matched: true, action, interceptor };
|
|
107
|
+
}
|
|
108
|
+
return { matched: false, action: null };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if a response status code should trigger a default retry.
|
|
112
|
+
* Only called when no interceptor matched the URL.
|
|
113
|
+
* Returns the error type if retry should happen, or null if response is fine.
|
|
114
|
+
*/
|
|
115
|
+
function checkDefaultRetryStatus(status) {
|
|
116
|
+
return DEFAULT_RETRY_STATUSES[status] ?? null;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/classifier.ts"],"names":[],"mappings":"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"}
|