11import { Effect , Schema } from "effect"
22import { HttpClient , HttpClientRequest } from "effect/unstable/http"
3+ import { FetchUse } from "@browser-use/bcode-browser/fetch-use"
34import * as Tool from "./tool"
45import TurndownService from "turndown"
56import DESCRIPTION from "./webfetch.txt"
@@ -24,6 +25,7 @@ export const WebFetchTool = Tool.define(
2425 Effect . gen ( function * ( ) {
2526 const http = yield * HttpClient . HttpClient
2627 const httpOk = HttpClient . filterStatusOk ( http )
28+ const fetchUse = yield * FetchUse . Service
2729
2830 return {
2931 description : DESCRIPTION ,
@@ -47,61 +49,77 @@ export const WebFetchTool = Tool.define(
4749
4850 const timeout = Math . min ( ( params . timeout ?? DEFAULT_TIMEOUT / 1000 ) * 1000 , MAX_TIMEOUT )
4951
50- // Build Accept header based on requested format with q parameters for fallbacks
51- let acceptHeader = "*/*"
52- switch ( params . format ) {
53- case "markdown" :
54- acceptHeader = "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
55- break
56- case "text" :
57- acceptHeader = "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1"
58- break
59- case "html" :
60- acceptHeader =
61- "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1"
62- break
63- default :
64- acceptHeader =
65- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
66- }
67- const headers = {
68- "User-Agent" :
69- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" ,
70- Accept : acceptHeader ,
71- "Accept-Language" : "en-US,en;q=0.9" ,
72- }
52+ // BrowserCode: when fetch-use is enabled (BROWSER_USE_API_KEY set,
53+ // BCODE_NO_FETCH_USE != "1"), proxy through it for Chrome JA4
54+ // fingerprinting + HTTP/2 header order. Falls back to native
55+ // HttpClient with cloudflare-retry when disabled.
56+ const { arrayBuffer, contentType } = yield * ( fetchUse . enabled
57+ ? fetchUse
58+ . fetch ( params . url , { timeoutMs : timeout } )
59+ . pipe ( Effect . map ( ( r ) => ( { arrayBuffer : r . body , contentType : r . contentType } ) ) )
60+ : Effect . gen ( function * ( ) {
61+ // Build Accept header based on requested format with q parameters for fallbacks
62+ let acceptHeader = "*/*"
63+ switch ( params . format ) {
64+ case "markdown" :
65+ acceptHeader =
66+ "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
67+ break
68+ case "text" :
69+ acceptHeader = "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1"
70+ break
71+ case "html" :
72+ acceptHeader =
73+ "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1"
74+ break
75+ default :
76+ acceptHeader =
77+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
78+ }
79+ const headers = {
80+ "User-Agent" :
81+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" ,
82+ Accept : acceptHeader ,
83+ "Accept-Language" : "en-US,en;q=0.9" ,
84+ }
7385
74- const request = HttpClientRequest . get ( params . url ) . pipe ( HttpClientRequest . setHeaders ( headers ) )
75-
76- // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
77- const response = yield * httpOk . execute ( request ) . pipe (
78- Effect . catchIf (
79- ( err ) =>
80- err . reason . _tag === "StatusCodeError" &&
81- err . reason . response . status === 403 &&
82- err . reason . response . headers [ "cf-mitigated" ] === "challenge" ,
83- ( ) =>
84- httpOk . execute (
85- HttpClientRequest . get ( params . url ) . pipe (
86- HttpClientRequest . setHeaders ( { ...headers , "User-Agent" : "browsercode" } ) ,
86+ const request = HttpClientRequest . get ( params . url ) . pipe ( HttpClientRequest . setHeaders ( headers ) )
87+
88+ // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
89+ const response = yield * httpOk . execute ( request ) . pipe (
90+ Effect . catchIf (
91+ ( err ) =>
92+ err . reason . _tag === "StatusCodeError" &&
93+ err . reason . response . status === 403 &&
94+ err . reason . response . headers [ "cf-mitigated" ] === "challenge" ,
95+ ( ) =>
96+ httpOk . execute (
97+ HttpClientRequest . get ( params . url ) . pipe (
98+ HttpClientRequest . setHeaders ( { ...headers , "User-Agent" : "browsercode" } ) ,
99+ ) ,
100+ ) ,
87101 ) ,
88- ) ,
89- ) ,
90- Effect . timeoutOrElse ( { duration : timeout , orElse : ( ) => Effect . die ( new Error ( "Request timed out" ) ) } ) ,
91- )
92-
93- // Check content length
94- const contentLength = response . headers [ "content-length" ]
95- if ( contentLength && parseInt ( contentLength ) > MAX_RESPONSE_SIZE ) {
96- throw new Error ( "Response too large (exceeds 5MB limit)" )
97- }
102+ Effect . timeoutOrElse ( { duration : timeout , orElse : ( ) => Effect . die ( new Error ( "Request timed out" ) ) } ) ,
103+ )
104+
105+ // Check content length
106+ const contentLength = response . headers [ "content-length" ]
107+ if ( contentLength && parseInt ( contentLength ) > MAX_RESPONSE_SIZE ) {
108+ throw new Error ( "Response too large (exceeds 5MB limit)" )
109+ }
110+
111+ const arrayBuffer = yield * response . arrayBuffer
112+ if ( arrayBuffer . byteLength > MAX_RESPONSE_SIZE ) {
113+ throw new Error ( "Response too large (exceeds 5MB limit)" )
114+ }
115+
116+ return { arrayBuffer, contentType : response . headers [ "content-type" ] || "" }
117+ } ) )
98118
99- const arrayBuffer = yield * response . arrayBuffer
100119 if ( arrayBuffer . byteLength > MAX_RESPONSE_SIZE ) {
101120 throw new Error ( "Response too large (exceeds 5MB limit)" )
102121 }
103122
104- const contentType = response . headers [ "content-type" ] || ""
105123 const mime = contentType . split ( ";" ) [ 0 ] ?. trim ( ) . toLowerCase ( ) || ""
106124 const title = `${ params . url } (${ contentType } )`
107125
0 commit comments