1import http from 'node:http';
2import { Actor, log } from 'apify';
3import { executeRequest } from './http-utils.js';
4
5const PPE_EVENT = 'http-request';
6
7await Actor.init();
8
9if (Actor.config.get('metaOrigin') === 'STANDBY') {
10 startStandbyServer();
11} else {
12 await runBatchMode();
13 await Actor.exit();
14}
15
16
17
18
19
20function startStandbyServer() {
21 const port = Actor.config.get('containerPort');
22
23 const server = http.createServer(async (req, res) => {
24
25 if (req.headers['x-apify-container-server-readiness-probe']) {
26 res.writeHead(200, { 'Content-Type': 'application/json' });
27 res.end(JSON.stringify({ status: 'ready' }));
28 return;
29 }
30
31
32 if (req.method === 'GET') {
33 res.writeHead(200, { 'Content-Type': 'text/html' });
34 res.end(getLandingPageHtml());
35 return;
36 }
37
38
39 if (req.method !== 'POST') {
40 res.writeHead(405, { 'Content-Type': 'application/json' });
41 res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
42 return;
43 }
44
45 try {
46 const body = await readRequestBody(req);
47 let params;
48 try {
49 params = JSON.parse(body);
50 } catch {
51 const err = new Error('Invalid JSON request body.');
52 err.statusCode = 400;
53 throw err;
54 }
55
56 if (!params.endpoint) {
57 const err = new Error('Missing required field: endpoint.');
58 err.statusCode = 400;
59 throw err;
60 }
61
62
63 if (!params.method) {
64 params.method = 'GET';
65 }
66
67 const result = await executeRequest(params);
68 await Actor.pushData(result);
69
70
71 try {
72 await Actor.charge({ eventName: PPE_EVENT, count: 1 });
73 } catch (chargeErr) {
74 console.warn(`PPE charge failed: ${chargeErr.message}`);
75 }
76
77 res.writeHead(200, { 'Content-Type': 'application/json' });
78 res.end(JSON.stringify(result));
79 } catch (err) {
80 const statusCode = err.statusCode || 502;
81 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
82 res.end(JSON.stringify({ error: err.message }));
83 }
84 });
85
86 server.listen(port, () => {
87 log.info(`HTTP API Standby server listening on port ${port}`);
88 });
89}
90
91function readRequestBody(req) {
92 return new Promise((resolve, reject) => {
93 const chunks = [];
94 req.on('data', (chunk) => chunks.push(chunk));
95 req.on('end', () => resolve(Buffer.concat(chunks).toString()));
96 req.on('error', reject);
97 });
98}
99
100
101
102
103
104async function runBatchMode() {
105 const {
106 method = 'GET',
107 endpoint,
108 body,
109 headers,
110 querystring,
111 response_headers,
112 timeoutSeconds = 30,
113 maxRetries = 3,
114 include_meta = false,
115 } = await Actor.getInput() ?? {};
116
117 if (!endpoint) {
118 await Actor.fail({
119 statusMessage: 'Missing required field: endpoint. Example input: { "endpoint": "https://catfact.ninja/fact", "method": "GET" }',
120 });
121 return;
122 }
123
124 log.info('Starting request', { method, endpoint });
125
126 const result = await executeRequest({
127 method,
128 endpoint,
129 body,
130 headers,
131 querystring,
132 response_headers,
133 timeoutSeconds,
134 maxRetries,
135 include_meta,
136 });
137
138 await Actor.pushData(result);
139
140
141 try {
142 await Actor.charge({ eventName: PPE_EVENT, count: 1 });
143 } catch (chargeErr) {
144 console.warn(`PPE charge failed: ${chargeErr.message}`);
145 }
146
147 if (result.status >= 400) {
148 log.warning(`Received HTTP ${result.status}`);
149 } else {
150 log.info('Request succeeded', { status: result.status });
151 }
152}
153
154
155
156
157
158function getLandingPageHtml() {
159 return `<!DOCTYPE html>
160<html lang="en">
161<head>
162<meta charset="utf-8">
163<meta name="viewport" content="width=device-width, initial-scale=1">
164<title>HTTP API — Superlative</title>
165<link rel="preconnect" href="https://fonts.googleapis.com">
166<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
167<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
168<style>
169:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
170*{margin:0;padding:0;box-sizing:border-box}
171body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
172a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
173a:hover{color:#6ee7a0}
174.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
175.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
176@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
177@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
178.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
179header{padding:32px 0}
180.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
181.logo:hover{color:var(--color-text)}
182.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
183.logo-mark svg{width:16px;height:16px}
184.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
185.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
186.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
187h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
188h1 .highlight{color:var(--color-accent)}
189.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
190.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
191.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
192.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
193h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
194p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
195li{margin-bottom:6px}
196ul{margin:.5rem 0 1rem 1.25rem}
197strong{color:var(--color-text);font-weight:600}
198pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
199code{font-family:var(--font-display);font-size:13px}
200p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
201table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
202th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
203th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
204td{color:var(--color-text-muted)}
205.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
206.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
207.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
208.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
209.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
210.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
211.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
212footer{padding:48px 0;border-top:1px solid var(--color-border)}
213.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
214.footer-text{font-size:13px;color:var(--color-text-muted)}
215.footer-links{display:flex;gap:24px}
216.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
217.footer-links a:hover{color:var(--color-accent)}
218@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
219</style>
220</head>
221<body>
222<div class="grid-bg"></div>
223<div class="accent-line"></div>
224
225<div class="container">
226 <header>
227 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
228 <div class="logo-mark">
229 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
230 <polyline points="4 17 10 11 4 5"></polyline>
231 <line x1="12" y1="19" x2="20" y2="19"></line>
232 </svg>
233 </div>
234 Superlative
235 </a>
236 </header>
237
238 <section class="hero">
239 <div class="hero-label">Utilities</div>
240 <h1>HTTP <span class="highlight">API</span></h1>
241 <p class="hero-desc">General-purpose HTTP request proxy. Make requests to any endpoint through Apify's infrastructure with sub-second Standby response times.</p>
242 <a href="https://apify.com/superlativetech/http-api?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
243 </section>
244
245 <section class="section">
246 <div class="section-label">Features</div>
247 <h2>What this does</h2>
248 <ul>
249 <li><strong>All HTTP methods</strong> — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS</li>
250 <li><strong>Custom headers</strong> — send any request headers as key-value pairs</li>
251 <li><strong>Request body</strong> — JSON, form data, or raw text</li>
252 <li><strong>Query strings</strong> — append query parameters to any endpoint</li>
253 <li><strong>Retries</strong> — automatic retries with configurable count</li>
254 <li><strong>Response headers</strong> — optionally capture specific response headers</li>
255 </ul>
256 </section>
257
258 <section class="section">
259 <div class="section-label">Getting Started</div>
260 <h2>Quick start</h2>
261 <p>Send a POST request to the Standby URL with a JSON body. Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
262
263 <p style="margin-top:16px"><strong>Simple GET proxy:</strong></p>
264 <pre>curl -X POST "https://superlativetech--http-api.apify.actor?token=YOUR_TOKEN" \\
265 -H "Content-Type: application/json" \\
266 -d '{
267 "endpoint": "https://catfact.ninja/fact"
268 }'</pre>
269
270 <p><strong>POST with headers and body:</strong></p>
271 <pre>curl -X POST "https://superlativetech--http-api.apify.actor?token=YOUR_TOKEN" \\
272 -H "Content-Type: application/json" \\
273 -d '{
274 "endpoint": "https://httpbin.org/post",
275 "method": "POST",
276 "headers": [
277 {"key": "X-Custom", "value": "hello"}
278 ],
279 "body": {"message": "test"}
280 }'</pre>
281 </section>
282
283 <section class="section">
284 <div class="section-label">Reference</div>
285 <h2>Request body parameters</h2>
286 <table>
287 <tr><th>Field</th><th>Required</th><th>Default</th><th>Description</th></tr>
288 <tr><td><code>endpoint</code></td><td>Yes</td><td>—</td><td>Target URL to request</td></tr>
289 <tr><td><code>method</code></td><td>No</td><td>GET</td><td>HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)</td></tr>
290 <tr><td><code>headers</code></td><td>No</td><td>—</td><td>Array of <code>{"key": "...", "value": "..."}</code> objects</td></tr>
291 <tr><td><code>body</code></td><td>No</td><td>—</td><td>Request body (object or string)</td></tr>
292 <tr><td><code>querystring</code></td><td>No</td><td>—</td><td>Array of <code>{"key": "...", "value": "..."}</code> query params</td></tr>
293 <tr><td><code>response_headers</code></td><td>No</td><td>—</td><td>Array of header names to capture from the response</td></tr>
294 <tr><td><code>timeoutSeconds</code></td><td>No</td><td>30</td><td>Request timeout in seconds</td></tr>
295 <tr><td><code>maxRetries</code></td><td>No</td><td>3</td><td>Number of retry attempts on failure</td></tr>
296 <tr><td><code>include_meta</code></td><td>No</td><td>false</td><td>Include timing and redirect metadata</td></tr>
297 </table>
298 </section>
299
300 <section class="section">
301 <div class="section-label">Response</div>
302 <h2>Response format</h2>
303 <pre>{
304 "status": 200,
305 "headers": {},
306 "body": { "fact": "Cats sleep 70% of their lives.", "length": 31 }
307}</pre>
308 </section>
309
310 <section class="section">
311 <div class="section-label">Cost</div>
312 <h2>Pricing</h2>
313 <p>Pay-per-event pricing on the Apify platform:</p>
314 <table>
315 <tr><th>Tier</th><th>Requests</th><th>Per 1,000</th></tr>
316 <tr><td>Base</td><td>Any</td><td>$0.20</td></tr>
317 <tr><td><span class="badge">Bronze</span></td><td>100+</td><td>$0.18</td></tr>
318 <tr><td><span class="badge">Silver</span></td><td>1,000+</td><td>$0.16</td></tr>
319 <tr><td><span class="badge">Gold</span></td><td>10,000+</td><td>$0.14</td></tr>
320 </table>
321 </section>
322
323 <section class="section">
324 <div class="section-label">Auth</div>
325 <h2>Authentication</h2>
326 <p>Authenticate using either method:</p>
327 <ul>
328 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
329 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
330 </ul>
331 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
332 </section>
333
334 <section class="section">
335 <div class="section-label">Superlative</div>
336 <h2>More from Superlative</h2>
337 <div class="product-grid">
338 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
339 <div class="product-name">Company Names <span class="status">Live</span></div>
340 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
341 </a>
342 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
343 <div class="product-name">Job Titles <span class="status">Live</span></div>
344 <p class="product-desc">Standardize job titles for outreach sequences.</p>
345 </a>
346 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
347 <div class="product-name">Person Names <span class="status">Live</span></div>
348 <p class="product-desc">Clean and format person names for personalization.</p>
349 </a>
350 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
351 <div class="product-name">Product Names <span class="status">Live</span></div>
352 <p class="product-desc">Normalize product and brand names from exports.</p>
353 </a>
354 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="product-card">
355 <div class="product-name">Places <span class="status">Live</span></div>
356 <p class="product-desc">Parse and standardize location strings.</p>
357 </a>
358 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="product-card">
359 <div class="product-name">URLs <span class="status">Live</span></div>
360 <p class="product-desc">Clean and normalize URLs from lead data.</p>
361 </a>
362 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="product-card">
363 <div class="product-name">Phone Numbers <span class="status">Live</span></div>
364 <p class="product-desc">Format and validate phone numbers.</p>
365 </a>
366 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="product-card">
367 <div class="product-name">Supernet DNS Lookup <span class="status">Live</span></div>
368 <p class="product-desc">Instant DNS record lookups and reverse DNS.</p>
369 </a>
370 <a href="https://apify.com/superlativetech/supernet-domain-health?fpr=8e9l1" class="product-card">
371 <div class="product-name">Domain Health <span class="status">Live</span></div>
372 <p class="product-desc">Email reputation audit: SPF, DKIM, DMARC, blacklists, SSL.</p>
373 </a>
374 </div>
375 </section>
376
377 <footer>
378 <div class="footer-content">
379 <p class="footer-text">© 2026 Superlative</p>
380 <div class="footer-links">
381 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
382 </div>
383 </div>
384 </footer>
385</div>
386
387</body>
388</html>`;
389}