Skip to content

Commit 8d679cb

Browse files
fix(api) (firecrawl#3769)
Co-authored-by: mogery <mogery@sideguide.dev>
1 parent 6a3c5cb commit 8d679cb

4 files changed

Lines changed: 122 additions & 17 deletions

File tree

apps/api/src/__tests__/snips/v2/scrape-skip-tls.test.ts

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,122 @@
11
import {
22
ALLOW_TEST_SUITE_WEBSITE,
3+
HAS_PROXY,
34
TEST_PRODUCTION,
45
TEST_SUITE_WEBSITE,
56
testIf,
67
} from "../lib";
78
import { Identity, idmux, scrapeTimeout, scrape, scrapeRaw } from "./lib";
9+
import https from "node:https";
10+
11+
const SELF_SIGNED_KEY = `-----BEGIN PRIVATE KEY-----
12+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCBJPpxbzPPuW8
13+
DxlXh4JXR56Qq7hT74yiD+PzZ2OznSnfpZOA2kmQ30ztuGNstr/oE5Ixwarcp+9P
14+
0tFz321TeN/6PNmSb0MPay8Q9xjvylMSao2ydATTyKumoF0nqZfHlgQNu6uYGan7
15+
/8qmZcVIJ09oJP29vFTxP7S6Nk40klHlQyBAwtT+IoRqWvBztnu+5Xuy8cIQP4er
16+
WoKySMuuVsnU8kwwM+QbFKGipD1jnK1xUGaaUtNpStNqnUKwNtW4jsMRjR/FapPF
17+
p8hHxOGrgHtZJ7lqZHi60TdrnaPQjvwe+drswJIM9cIPkyzyd0FBlg+CPGkmlUAC
18+
4w0MjknjAgMBAAECggEAUyiXJWnlpYa1/UcTe5rPWQ2Pfz67AO75/jSFZbx41XGV
19+
kxBrCp2FTp0HYhTYOK6TzqskzELQM0efoT0hHWM0fsSea6lNMCCUQ7WTNNhUTeMQ
20+
fCJDnatwrj2ipQazJt7f+WHpVuGLiOPnIeXfPDb+uhBvTpocUAi697RwiCfimVEl
21+
wOcETyKjG+AdkHn1WqXdiduIQsmm3f8H4rJcGZo2OpyU33s0ZK2ReZhIkOEoyzp+
22+
BX9cN8wnmiQc4M0DpcSOZYwn2OtLdsGirFasmhfDFMBgE+BS9W3knsMxd/zEtKTs
23+
UxTK6bfdCNJkE8fR0H0ozViFvl61fcPuBvyDeTorwQKBgQD3oSbHo6MfZ53vNbBu
24+
rfzoV35jnWUN34fkMo8YkGGIH1nPb7xlShg/zR6OL86wk6ADFO43UFEmbVDGxlCn
25+
OscA04pfKP/Zvb9EDVr/j5Ix1LiFlATbjkPPqZj3IRdIih0ycjKFHR/OtbpPoINT
26+
8CNs1/VYsgYh47t9fdApqdc4MQKBgQDIk4BGQfYc4Fzxpfhf7/IwA5AfTJip/H9f
27+
XAxVFJhANymaUdDxar9D7VdoSQYOL2BD7KzT0JeeFwvwavnVc0jATAgmi81D+EaF
28+
RX5DG5GoYSFD8aSnhu0dGF3QrPemaamlJdCYOd3of+b0wZhkCvVPcOy/bTdkOD9q
29+
F3gZacWyUwKBgQCcbbjJtJ1/YT1rt0bVJCP3wg2db/g+Y0684RN0OQDjtKpPWA5z
30+
DfNzmmgK+jhfY2JZkAdL+fjJhZTZfL3GZmMAKqHmq0e0jSEeJDGv70ozIGXQPEk1
31+
SRGdRU3UD5tdv6HiFDHF1TgapMIlOwi7JZ+7SlE6znsBPZNbGvc82oWSgQKBgQDA
32+
rVEQLNUrsCwYxoLuRiW1Efck2gPdZ31EMbx3Dq2jIlqIsAezogPSUPEicOOsRL6J
33+
AZaUc1DywNjrPxh4f6Jnd6JsxOeOX7X+2F8OQDGQOp4mEr9FX0vwIzQb/cx4xA//
34+
YXAci5osepF2lXK6x/wXMDd9PIF1eMMSOzFX2E/dmQKBgCAfdmgO9yU1K2esVb8B
35+
vLPuFWNiQ1UvmDaV4DDc10JQH64R/yv7HsmRf5C+oPjUbgfXyn+SLdtaVKWtyDtk
36+
8ajWWfZ2tqtXT31VdeJLjkABT+BU9H3u5YyrSPNIcGMgNSLjmb/DuWaclrIT6+hL
37+
9g/pFxnn4mOVBYdX1GyDHhBD
38+
-----END PRIVATE KEY-----`;
39+
40+
const SELF_SIGNED_CERT = `-----BEGIN CERTIFICATE-----
41+
MIICyTCCAbGgAwIBAgIJAMikv3+5vuPnMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
42+
BAMMCWxvY2FsaG9zdDAeFw0yNjA2MjIxNzQ0NThaFw0zNjA2MTkxNzQ0NThaMBQx
43+
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
44+
ggEBAMIEk+nFvM8+5bwPGVeHgldHnpCruFPvjKIP4/NnY7OdKd+lk4DaSZDfTO24
45+
Y2y2v+gTkjHBqtyn70/S0XPfbVN43/o82ZJvQw9rLxD3GO/KUxJqjbJ0BNPIq6ag
46+
XSepl8eWBA27q5gZqfv/yqZlxUgnT2gk/b28VPE/tLo2TjSSUeVDIEDC1P4ihGpa
47+
8HO2e77le7LxwhA/h6tagrJIy65WydTyTDAz5BsUoaKkPWOcrXFQZppS02lK02qd
48+
QrA21biOwxGNH8Vqk8WnyEfE4auAe1knuWpkeLrRN2udo9CO/B752uzAkgz1wg+T
49+
LPJ3QUGWD4I8aSaVQALjDQyOSeMCAwEAAaMeMBwwGgYDVR0RBBMwEYIJbG9jYWxo
50+
b3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCqlZaFVPZ69S25OVzEPiRClsEe
51+
NcpBmGwwEl3Yn+hmMXspBaubv2cHWKL+KRm3C9FicD/FqwTxjVqBmizZRFUETS/r
52+
vTBEKE2lwXUVUbMZtCJ+NPszXh00PFjHO/Z/1poG+ZiMEXWB+nYJRDxUIt5BmK9W
53+
GRxVxKHLId8rqcGyQk/hdjhg8xd9jINKN6T378ZHv1t+Z1WAWLwA4oZeiWwD7kEF
54+
3IDQNbcl65aq2Qw6IDw+6bnCLc54A3BiU7miHDlNiP9rgjqf3cYzutTiem8L7DbY
55+
SDXcSDjxdKJmewIopBnycE2rAU4/O7AivhFA0iqQ/KAD27s6l/7rtO+LH6za
56+
-----END CERTIFICATE-----`;
857

958
describe("V2 Scrape skipTlsVerification Default", () => {
1059
let identity: Identity;
60+
let tlsServer: https.Server;
61+
let selfSignedUrl: string;
1162

1263
beforeAll(async () => {
1364
identity = await idmux({
1465
name: "v2-scrape-skip-tls",
1566
concurrency: 100,
1667
credits: 1000000,
1768
});
69+
70+
tlsServer = https.createServer(
71+
{ key: SELF_SIGNED_KEY, cert: SELF_SIGNED_CERT },
72+
(_req, res) => {
73+
res.setHeader("Content-Type", "text/html; charset=utf-8");
74+
res.end("<main><h1>Self-signed TLS fixture</h1></main>");
75+
},
76+
);
77+
78+
await new Promise<void>(resolve => {
79+
tlsServer.listen(0, "127.0.0.1", resolve);
80+
});
81+
const address = tlsServer.address();
82+
if (address === null || typeof address === "string") {
83+
throw new Error("Failed to start self-signed TLS fixture");
84+
}
85+
selfSignedUrl = `https://127.0.0.1:${address.port}/`;
1886
}, 10000);
1987

20-
test(
88+
afterAll(async () => {
89+
if (!tlsServer) {
90+
return;
91+
}
92+
await new Promise<void>((resolve, reject) => {
93+
tlsServer.close(err => (err ? reject(err) : resolve()));
94+
});
95+
});
96+
97+
testIf(!HAS_PROXY)(
2198
"should default skipTlsVerification to true in v2 API",
2299
async () => {
23100
const data = await scrape(
24101
{
25-
url: "https://expired.badssl.com/",
102+
url: selfSignedUrl,
26103
maxAge: 0,
27104
},
28105
identity,
29106
);
30107

31108
expect(data).toBeDefined();
32-
expect(data.markdown).toContain("badssl.com");
109+
expect(data.markdown).toContain("Self-signed TLS fixture");
33110
},
34111
scrapeTimeout,
35112
);
36113

37-
test(
114+
testIf(!HAS_PROXY)(
38115
"should allow explicit skipTlsVerification: false override",
39116
async () => {
40117
const response = await scrapeRaw(
41118
{
42-
url: "https://expired.badssl.com/",
119+
url: selfSignedUrl,
43120
skipTlsVerification: false,
44121
maxAge: 0,
45122
},

apps/api/src/__tests__/snips/v2/types-validation.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ describe("V2 Types Validation", () => {
3838
expect(result.url).toBe("https://example.com");
3939
expect(result.origin).toBe("api");
4040
expect(result.formats).toEqual([{ type: "markdown" }]);
41+
expect(result.skipTlsVerification).toBe(true);
42+
});
43+
44+
it("should preserve explicit skipTlsVerification false", () => {
45+
const input: ScrapeRequestInput = {
46+
url: "https://example.com",
47+
skipTlsVerification: false,
48+
};
49+
50+
const result = scrapeRequestSchema.parse(input);
51+
expect(result.skipTlsVerification).toBe(false);
52+
});
53+
54+
it("should default skipTlsVerification to false when custom headers are used", () => {
55+
const input: ScrapeRequestInput = {
56+
url: "https://example.com",
57+
headers: {
58+
"x-test": "true",
59+
},
60+
};
61+
62+
const result = scrapeRequestSchema.parse(input);
63+
expect(result.skipTlsVerification).toBe(false);
4164
});
4265

4366
it("should accept valid scrape request with format objects", () => {

apps/api/src/controllers/v2/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,14 +724,26 @@ const waitForRefineOpts = {
724724
path: ["waitFor"],
725725
};
726726

727+
export const applyScrapeOptionsDefaults = <T extends ScrapeOptionsBase>(
728+
obj: T,
729+
): T & { skipTlsVerification: boolean } => ({
730+
...obj,
731+
skipTlsVerification:
732+
obj.skipTlsVerification ??
733+
((obj.headers && Object.keys(obj.headers).length > 0) ||
734+
(obj.actions && obj.actions.length > 0)
735+
? false
736+
: true),
737+
});
738+
727739
// Base transform function that handles both nullable and non-nullable cases
728740
// Uses generic type to preserve all fields from extended schemas
729741
const extractTransformImpl = <T extends ScrapeOptionsBase | undefined>(
730742
obj: T,
731743
): T extends undefined ? undefined : T => {
732744
if (!obj) return obj as T extends undefined ? undefined : T;
733745
// Handle timeout
734-
let result = { ...obj };
746+
let result = applyScrapeOptionsDefaults(obj);
735747
if (
736748
obj.formats.find(x => typeof x === "object" && x.type === "json") &&
737749
obj.timeout === 30000

apps/api/src/scraper/scrapeURL/index.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { withSpan, setSpanAttributes } from "../../lib/otel-tracer";
55
import { captureExceptionWithZdrCheck } from "../../services/sentry";
66

77
import {
8+
applyScrapeOptionsDefaults,
89
type Document,
910
getPDFMaxPages,
1011
scrapeOptions,
@@ -416,21 +417,13 @@ async function buildMetaObject(
416417
}
417418
}
418419

419-
const normalizedOptions = {
420-
...options,
421-
skipTlsVerification:
422-
options.skipTlsVerification ??
423-
((options.headers && Object.keys(options.headers).length > 0) ||
424-
(options.actions && options.actions.length > 0)
425-
? false
426-
: true),
427-
};
420+
const effectiveOptions = applyScrapeOptionsDefaults(options);
428421

429422
return {
430423
id,
431424
url,
432425
rewrittenUrl: rewriteUrl(url),
433-
options: normalizedOptions,
426+
options: effectiveOptions,
434427
internalOptions,
435428
logger,
436429
abortHandle,
@@ -447,7 +440,7 @@ async function buildMetaObject(
447440
}
448441
: undefined,
449442
),
450-
featureFlags: buildFeatureFlags(url, normalizedOptions, internalOptions),
443+
featureFlags: buildFeatureFlags(url, effectiveOptions, internalOptions),
451444
mock:
452445
options.useMock !== undefined
453446
? await loadMock(options.useMock, _logger)

0 commit comments

Comments
 (0)