1- import { devices , type Page } from "playwright" ;
1+ import {
2+ devices ,
3+ type Page ,
4+ type Response as PlaywrightResponse ,
5+ } from "playwright" ;
26import type { Response } from "../protocol.ts" ;
37import { handleSnapshot } from "./snapshot.ts" ;
48import { PRESETS , type ViewportParsedArgs } from "./viewport.ts" ;
@@ -102,6 +106,79 @@ function parseGotoArgs(args: string[]): {
102106 return { url, viewport : null } ;
103107}
104108
109+ function trimSnippet ( text : string ) : string {
110+ return text . replace ( / \s + / g, " " ) . trim ( ) . slice ( 0 , 500 ) ;
111+ }
112+
113+ function headerValue (
114+ headers : Record < string , string > ,
115+ name : string ,
116+ ) : string | undefined {
117+ return headers [ name ] ?? headers [ name . toLowerCase ( ) ] ;
118+ }
119+
120+ async function getBodySnippet ( page : Page ) : Promise < string > {
121+ try {
122+ return trimSnippet (
123+ await page . locator ( "body" ) . innerText ( { timeout : 2_000 } ) ,
124+ ) ;
125+ } catch {
126+ return "" ;
127+ }
128+ }
129+
130+ function looksLikeCdnAccessBlock (
131+ status : number ,
132+ headers : Record < string , string > ,
133+ title : string ,
134+ bodySnippet : string ,
135+ ) : boolean {
136+ if ( status !== 403 ) return false ;
137+
138+ const server = headerValue ( headers , "server" ) ?. toLowerCase ( ) ?? "" ;
139+ const combined = `${ title } \n${ bodySnippet } ` . toLowerCase ( ) ;
140+
141+ return (
142+ server . includes ( "akamaighost" ) ||
143+ server . includes ( "akamai" ) ||
144+ ( combined . includes ( "access denied" ) &&
145+ ( combined . includes ( "edgesuite" ) || combined . includes ( "permission" ) ) )
146+ ) ;
147+ }
148+
149+ async function cdnBlockDiagnostic (
150+ page : Page ,
151+ response : PlaywrightResponse ,
152+ title : string ,
153+ ) : Promise < string | null > {
154+ const status = response . status ( ) ;
155+ const headers = response . headers ( ) ;
156+ const bodySnippet = await getBodySnippet ( page ) ;
157+
158+ if ( ! looksLikeCdnAccessBlock ( status , headers , title , bodySnippet ) ) {
159+ return null ;
160+ }
161+
162+ const server = headerValue ( headers , "server" ) ?? "unknown" ;
163+ const finalUrl = response . url ( ) ;
164+ const lines = [
165+ `Navigation blocked by CDN/access controls (${ status } ).` ,
166+ `URL: ${ finalUrl } ` ,
167+ `Server: ${ server } ` ,
168+ `Title: ${ title || "untitled" } ` ,
169+ ] ;
170+
171+ if ( bodySnippet ) {
172+ lines . push ( `Body: ${ bodySnippet } ` ) ;
173+ }
174+
175+ lines . push (
176+ "Try a real browser session, configured proxy, or verify that the installed 'browse' binary is the project CLI with 'browse version'." ,
177+ ) ;
178+
179+ return lines . join ( "\n" ) ;
180+ }
181+
105182export async function handleGoto (
106183 page : Page ,
107184 args : string [ ] ,
@@ -126,7 +203,10 @@ export async function handleGoto(
126203 } ) ;
127204 }
128205
129- await page . goto ( url , { waitUntil : "domcontentloaded" , timeout : 30_000 } ) ;
206+ const navigationResponse = await page . goto ( url , {
207+ waitUntil : "domcontentloaded" ,
208+ timeout : 30_000 ,
209+ } ) ;
130210
131211 // Inject stealth patches to fix CreepJS detection.
132212 // This runs on every navigation since addInitScript only affects new pages.
@@ -169,6 +249,17 @@ export async function handleGoto(
169249
170250 const title = await page . title ( ) ;
171251
252+ if ( navigationResponse ) {
253+ const blockDiagnostic = await cdnBlockDiagnostic (
254+ page ,
255+ navigationResponse ,
256+ title ,
257+ ) ;
258+ if ( blockDiagnostic ) {
259+ return { ok : false , error : blockDiagnostic } ;
260+ }
261+ }
262+
172263 let result : string ;
173264 if ( viewport && viewport . action === "set" ) {
174265 const suffix = viewport . label ? ` (${ viewport . label } )` : "" ;
0 commit comments