@@ -13,6 +13,7 @@ import * as GitCafeCredentials from "./GitCafeCredentials.ts";
1313import * as GitCafeHosts from "./gitCafeHosts.ts" ;
1414
1515const NO_REDIRECT : RequestInit = { redirect : "manual" } ;
16+ const MAX_REDIRECTS = 3 ;
1617const TIMEOUT = Duration . seconds ( 30 ) ;
1718
1819export const GitCafeApiFailure = Schema . Literals ( [
@@ -85,6 +86,28 @@ const credentialFailure = (
8586 ? "unauthenticated"
8687 : "failed" ;
8788
89+ const REDIRECTS = new Set ( [ 301 , 302 , 303 , 307 , 308 ] ) ;
90+ // 307 and 308 keep the method and body; the others only safely repeat a read.
91+ const METHOD_PRESERVING = new Set ( [ 307 , 308 ] ) ;
92+
93+ const redirectTarget = (
94+ method : string ,
95+ origin : string ,
96+ from : string ,
97+ response : { readonly status : number ; readonly location : string | undefined } ,
98+ ) : string | null => {
99+ if ( ! REDIRECTS . has ( response . status ) || response . location === undefined ) return null ;
100+ if ( method !== "GET" && ! METHOD_PRESERVING . has ( response . status ) ) return null ;
101+ const target = URL . parse ( response . location , from ) ;
102+ return target !== null &&
103+ target . origin === origin &&
104+ target . username === "" &&
105+ target . password === "" &&
106+ target . pathname . startsWith ( "/api/" )
107+ ? target . href
108+ : null ;
109+ } ;
110+
88111/** @public */
89112export const make = Effect . gen ( function * ( ) {
90113 const httpClient = yield * HttpClient . HttpClient ;
@@ -122,30 +145,49 @@ export const make = Effect.gen(function* () {
122145 fail ( { status : null , reason : credentialFailure ( cause ) , detail : cause . message , cause } ) ,
123146 ) ,
124147 ) ;
125- const base = HttpClientRequest . make ( input . method ?? "GET" ) (
126- `https://${ host } /api${ input . path } ` ,
127- ) . pipe (
128- HttpClientRequest . bearerToken ( Redacted . value ( credential . token ) ) ,
129- HttpClientRequest . setHeaders ( { accept : "application/json" , "user-agent" : "t3code" } ) ,
130- ) ;
131- const { status, text } = yield * Effect . gen ( function * ( ) {
132- const response = yield * httpClient
133- . execute (
134- input . body === undefined ? base : base . pipe ( HttpClientRequest . bodyJsonUnsafe ( input . body ) ) ,
135- )
136- . pipe (
137- // Client spans expose query strings containing branch names.
138- Effect . provideService ( HttpClient . TracerDisabledWhen , ( ) => true ) ,
139- // GitCafe does not redirect; reject 3xx to protect bearer credentials.
140- Effect . provideService ( FetchHttpClient . RequestInit , NO_REDIRECT ) ,
141- ) ;
142- return { status : response . status , text : yield * response . text } ;
143- } ) . pipe (
144- Effect . timeout ( TIMEOUT ) ,
145- Effect . mapError ( ( cause ) =>
146- fail ( { status : null , reason : "failed" , detail : "Could not reach GitCafe." , cause } ) ,
147- ) ,
148- ) ;
148+ const origin = `https://${ host } ` ;
149+ const method = input . method ?? "GET" ;
150+ const send = ( url : string ) => {
151+ const base = HttpClientRequest . make ( method ) ( url ) . pipe (
152+ HttpClientRequest . bearerToken ( Redacted . value ( credential . token ) ) ,
153+ HttpClientRequest . setHeaders ( { accept : "application/json" , "user-agent" : "t3code" } ) ,
154+ ) ;
155+ return Effect . gen ( function * ( ) {
156+ const response = yield * httpClient
157+ . execute (
158+ input . body === undefined
159+ ? base
160+ : base . pipe ( HttpClientRequest . bodyJsonUnsafe ( input . body ) ) ,
161+ )
162+ . pipe (
163+ // Client spans expose query strings containing branch names.
164+ Effect . provideService ( HttpClient . TracerDisabledWhen , ( ) => true ) ,
165+ // fetch would carry the bearer token to any redirect target.
166+ Effect . provideService ( FetchHttpClient . RequestInit , NO_REDIRECT ) ,
167+ ) ;
168+ return {
169+ status : response . status ,
170+ location : response . headers . location ,
171+ text : yield * response . text ,
172+ } ;
173+ } ) . pipe (
174+ Effect . timeout ( TIMEOUT ) ,
175+ Effect . mapError ( ( cause ) =>
176+ fail ( { status : null , reason : "failed" , detail : "Could not reach GitCafe." , cause } ) ,
177+ ) ,
178+ ) ;
179+ } ;
180+ // A renumbered pull request (provisional number replaced by the synced GitHub one)
181+ // answers with a redirect; follow it only within this host's API.
182+ let url = `${ origin } /api${ input . path } ` ;
183+ let response = yield * send ( url ) ;
184+ for ( let hop = 0 ; hop < MAX_REDIRECTS ; hop ++ ) {
185+ const next = redirectTarget ( method , origin , url , response ) ;
186+ if ( next === null ) break ;
187+ url = next ;
188+ response = yield * send ( url ) ;
189+ }
190+ const { status, text } = response ;
149191 yield * Effect . annotateCurrentSpan ( { "http.response.status_code" : status } ) ;
150192 if ( status >= 200 && status < 300 ) return text ;
151193 // The source may hold a newer token than the one that was refused.
0 commit comments