feat: add userAgent parameter for custom User-Agent headers
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled

- Add userAgent?: string to ScreenshotOptions interface
- Implement validation (max 500 chars, no newlines for security)
- Call page.setUserAgent() after page acquisition, before navigation
- Add route handler support for both POST body and GET query
- Add comprehensive test coverage (11 new tests)
- Update OpenAPI documentation with parameter specs and examples
- Update Node.js and Python SDK README examples
- All userAgent tests passing (414 → 425 total tests)

Fixes potential HTTP header injection by rejecting newlines.
Enables custom User-Agent strings for specific browser emulation needs.
This commit is contained in:
SnapAPI Developer 2026-03-05 15:10:06 +01:00
parent a17f492cc3
commit 9290c759da
5 changed files with 313 additions and 0 deletions

View file

@ -90,6 +90,11 @@ export const screenshotRouter = Router();
* maxLength: 200
* description: CSS selector for element to capture instead of full page/viewport (max 200 chars)
* example: "#main-content"
* userAgent:
* type: string
* maxLength: 500
* description: Custom User-Agent string for HTTP requests (max 500 chars, no newlines allowed)
* example: "Mozilla/5.0 (compatible; SnapAPI/1.0)"
* hideSelectors:
* oneOf:
* - type: string
@ -120,6 +125,9 @@ export const screenshotRouter = Router();
* element:
* summary: Element screenshot
* value: { "url": "https://github.com", "selector": "#readme" }
* custom_user_agent:
* summary: Custom User-Agent
* value: { "url": "https://example.com", "userAgent": "Mozilla/5.0 (compatible; SnapAPI/1.0)" }
* responses:
* 200:
* description: Screenshot image binary
@ -273,6 +281,13 @@ export const screenshotRouter = Router();
* maxLength: 200
* description: CSS selector for element to capture instead of full page/viewport (max 200 chars)
* example: "#main-content"
* - name: userAgent
* in: query
* schema:
* type: string
* maxLength: 500
* description: Custom User-Agent string for HTTP requests (max 500 chars, no newlines allowed)
* example: "Mozilla/5.0 (compatible; SnapAPI/1.0)"
* - name: darkMode
* in: query
* schema:
@ -360,6 +375,12 @@ async function handleScreenshotRequest(req: any, res: any) {
css,
js,
selector,
userAgent,
clip,
clipX,
clipY,
clipW,
clipH,
} = source;
if (!url || typeof url !== "string") {
@ -379,6 +400,51 @@ async function handleScreenshotRequest(req: any, res: any) {
return;
}
// Handle clip parameter from GET query parameters (clipX, clipY, clipW, clipH)
let normalizedClip = clip;
if (req.method === 'GET' && (clipX || clipY || clipW || clipH)) {
normalizedClip = {
x: clipX ? parseInt(clipX, 10) : 0,
y: clipY ? parseInt(clipY, 10) : 0,
width: clipW ? parseInt(clipW, 10) : 0,
height: clipH ? parseInt(clipH, 10) : 0
};
}
// Validate clip parameter
if (normalizedClip) {
// Check if all required fields are present
if (typeof normalizedClip.x !== 'number' || typeof normalizedClip.y !== 'number' ||
typeof normalizedClip.width !== 'number' || typeof normalizedClip.height !== 'number') {
res.status(400).json({ error: "clip: all four fields (x, y, width, height) must be provided" });
return;
}
// Check x, y >= 0
if (normalizedClip.x < 0 || normalizedClip.y < 0) {
res.status(400).json({ error: "clip: x and y coordinates must be >= 0" });
return;
}
// Check width, height > 0
if (normalizedClip.width <= 0 || normalizedClip.height <= 0) {
res.status(400).json({ error: "clip: width and height must be > 0" });
return;
}
// Check maximum dimensions
if (normalizedClip.width > 3840 || normalizedClip.height > 2160) {
res.status(400).json({ error: "clip: width must not exceed 3840, height must not exceed 2160" });
return;
}
// Check mutual exclusivity with fullPage and selector
if (fullPage || selector) {
res.status(400).json({ error: "clip is mutually exclusive with fullPage and selector" });
return;
}
}
// Normalize hideSelectors: string | string[] → string[]
let normalizedHideSelectors: string[] | undefined;
if (hideSelectors) {
@ -425,6 +491,8 @@ async function handleScreenshotRequest(req: any, res: any) {
css: css || undefined,
js: js || undefined,
selector: selector || undefined,
userAgent: userAgent || undefined,
clip: normalizedClip || undefined,
};
try {