mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-15 06:16:15 +00:00
fix: use Vercel classic Node runtime API instead of Web Standards Request
Vercel `nodejs` runtime passes IncomingMessage/ServerResponse with shouldAddHelpers=true (auto-parsed JSON body, .status/.send helpers), not the Web Standards Request/Response. Calling `req.headers.get(...)` on the classic IncomingMessage threw `TypeError: req.headers.get is not a function` and crashed every webhook + cron invocation with 500. Switch both handlers to (req, res) signature, read headers as plain object (lowercased keys), use req.body for parsed JSON, and respond via res.status().send(). Caught during Phase 6 smoke test of the first prod deploy.
This commit is contained in:
1 parent
d9f23ee0c2
commit
b2082c4601
2 files changed
+29
-24
No files matched your search
+10
-8
@@ -1,21 +1,23 @@
|
|||||||
// Daily check entry. Triggered by Vercel Cron at 00:00 UTC = 07:00 Asia/Saigon
|
// Daily check entry. Triggered by Vercel Cron at 00:00 UTC = 07:00 Asia/Saigon
|
||||||
// (schedule lives in vercel.json). Replaces the prior Cloudflare Worker
|
// (schedule lives in vercel.json). Validates the Authorization: Bearer
|
||||||
// `scheduled` handler. Validates the Authorization: Bearer $CRON_SECRET header
|
// $CRON_SECRET header to prevent random POSTs from triggering the daily check.
|
||||||
// to prevent random POSTs from triggering the daily check.
|
//
|
||||||
|
// Vercel's `nodejs` runtime passes IncomingMessage/ServerResponse — req.headers
|
||||||
|
// is a plain object with lowercased keys, not a Web Headers instance.
|
||||||
|
|
||||||
import { buildApp } from '../src/app-builder.js';
|
import { buildApp } from '../src/app-builder.js';
|
||||||
import { runDailyCheck } from '../src/scheduler/scheduler.js';
|
import { runDailyCheck } from '../src/scheduler/scheduler.js';
|
||||||
|
|
||||||
export const config = { runtime: 'nodejs', maxDuration: 60 };
|
export const config = { runtime: 'nodejs', maxDuration: 60 };
|
||||||
|
|
||||||
export default async function handler(req) {
|
export default async function handler(req, res) {
|
||||||
// Fail closed if CRON_SECRET is unset — otherwise the comparison would be
|
// Fail closed if CRON_SECRET is unset — otherwise the comparison would be
|
||||||
// `Bearer undefined`, which an attacker could replay as a literal string
|
// `Bearer undefined`, which an attacker could replay as a literal string
|
||||||
// and bypass auth.
|
// and bypass auth.
|
||||||
const expected = process.env.CRON_SECRET;
|
const expected = process.env.CRON_SECRET;
|
||||||
const auth = req.headers.get('authorization');
|
const auth = req.headers['authorization'];
|
||||||
if (!expected || auth !== `Bearer ${expected}`) {
|
if (!expected || auth !== `Bearer ${expected}`) {
|
||||||
return new Response('Unauthorized', { status: 401 });
|
return res.status(401).send('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
let app;
|
let app;
|
||||||
@@ -23,10 +25,10 @@ export default async function handler(req) {
|
|||||||
app = buildApp(process.env);
|
app = buildApp(process.env);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(JSON.stringify({ level: 'error', msg: 'config error', err: err.message }));
|
console.log(JSON.stringify({ level: 'error', msg: 'config error', err: err.message }));
|
||||||
return new Response('Server misconfigured', { status: 500 });
|
return res.status(500).send('Server misconfigured');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cron runs synchronously up to maxDuration; no waitUntil needed.
|
// Cron runs synchronously up to maxDuration; no waitUntil needed.
|
||||||
await runDailyCheck(app.config, app.store, app.sender, app.appleScraper, app.googleScraper);
|
await runDailyCheck(app.config, app.store, app.sender, app.appleScraper, app.googleScraper);
|
||||||
return new Response('OK');
|
return res.status(200).send('OK');
|
||||||
}
|
}
|
||||||
+19
-16
@@ -1,7 +1,11 @@
|
|||||||
// Telegram webhook entry. Vercel serverless function — replaces the prior
|
// Telegram webhook entry. Vercel serverless function (classic Node runtime).
|
||||||
// Cloudflare Worker `fetch` handler. Validates the X-Telegram-Bot-Api-Secret-Token
|
// Validates the X-Telegram-Bot-Api-Secret-Token header, acks fast, then
|
||||||
// header, acks fast, then dispatches in waitUntil so Telegram doesn't retry on
|
// dispatches in waitUntil so Telegram doesn't retry on slow downstream calls.
|
||||||
// slow downstream calls.
|
//
|
||||||
|
// Vercel's `nodejs` runtime passes IncomingMessage/ServerResponse with
|
||||||
|
// `shouldAddHelpers: true` (so req.body is auto-parsed JSON and res has
|
||||||
|
// .status/.send helpers). req.headers is a plain object — header keys are
|
||||||
|
// lowercased.
|
||||||
|
|
||||||
import { waitUntil } from '@vercel/functions';
|
import { waitUntil } from '@vercel/functions';
|
||||||
import { buildApp } from '../src/app-builder.js';
|
import { buildApp } from '../src/app-builder.js';
|
||||||
@@ -9,9 +13,9 @@ import { dispatch } from '../src/bot/dispatch.js';
|
|||||||
|
|
||||||
export const config = { runtime: 'nodejs' };
|
export const config = { runtime: 'nodejs' };
|
||||||
|
|
||||||
export default async function handler(req) {
|
export default async function handler(req, res) {
|
||||||
if (req.method !== 'POST') {
|
if (req.method !== 'POST') {
|
||||||
return new Response('Not found', { status: 404 });
|
return res.status(404).send('Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
let app;
|
let app;
|
||||||
@@ -19,21 +23,20 @@ export default async function handler(req) {
|
|||||||
app = buildApp(process.env);
|
app = buildApp(process.env);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(JSON.stringify({ level: 'error', msg: 'config error', err: err.message }));
|
console.log(JSON.stringify({ level: 'error', msg: 'config error', err: err.message }));
|
||||||
return new Response('Server misconfigured', { status: 500 });
|
return res.status(500).send('Server misconfigured');
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = req.headers.get('x-telegram-bot-api-secret-token');
|
const secret = req.headers['x-telegram-bot-api-secret-token'];
|
||||||
if (secret !== app.config.telegramWebhookSecret) {
|
if (secret !== app.config.telegramWebhookSecret) {
|
||||||
return new Response('Unauthorized', { status: 401 });
|
return res.status(401).send('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
let update;
|
// req.body is auto-parsed by Vercel helpers when Content-Type is JSON.
|
||||||
try {
|
// Falsy / non-object guards mirror the prior CF handler.
|
||||||
update = await req.json();
|
const update = req.body;
|
||||||
} catch {
|
if (!update || typeof update !== 'object' || !update.message) {
|
||||||
return new Response('Bad request', { status: 400 });
|
return res.status(200).send('OK');
|
||||||
}
|
}
|
||||||
if (!update?.message) return new Response('OK');
|
|
||||||
|
|
||||||
waitUntil(
|
waitUntil(
|
||||||
dispatch(update.message, {
|
dispatch(update.message, {
|
||||||
@@ -43,5 +46,5 @@ export default async function handler(req) {
|
|||||||
logger: app.config.logger,
|
logger: app.config.logger,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return new Response('OK');
|
return res.status(200).send('OK');
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user