@@ -5,8 +5,10 @@ import { githubConnectorMeta } from '@/connectors/github/meta'
55import type { ConnectorConfig , ExternalDocument , ExternalDocumentList } from '@/connectors/types'
66import {
77 CONNECTOR_MAX_FILE_BYTES ,
8+ ConnectorFileTooLargeError ,
89 markSkipped ,
910 parseTagDate ,
11+ readBodyWithLimit ,
1012 sizeLimitSkipReason ,
1113 stubOrSkipBySize ,
1214 takeIndexableWithinCap ,
@@ -156,7 +158,7 @@ async function fetchBlobContent(
156158 const response = await fetchWithRetry ( url , {
157159 method : 'GET' ,
158160 headers : {
159- Accept : 'application/vnd.github+json' ,
161+ Accept : 'application/vnd.github.raw +json' ,
160162 Authorization : `Bearer ${ accessToken } ` ,
161163 'X-GitHub-Api-Version' : '2022-11-28' ,
162164 } ,
@@ -166,25 +168,20 @@ async function fetchBlobContent(
166168 throw new Error ( `Failed to fetch git blob ${ sha } : ${ response . status } ` )
167169 }
168170
169- const data = await response . json ( )
170- const content = ( data . content as string ) || ''
171- const encoding = data . encoding as string | undefined
171+ if ( ! response . body ) {
172+ const contentLength = Number . parseInt ( response . headers . get ( 'content-length' ) ?? '' , 10 )
173+ if ( Number . isFinite ( contentLength ) && contentLength > MAX_FILE_SIZE ) {
174+ throw new ConnectorFileTooLargeError ( MAX_FILE_SIZE )
175+ }
176+ throw new Error ( `GitHub git blob ${ sha } returned no body` )
177+ }
172178
173- if ( encoding === 'base64' ) {
174- const buf = Buffer . from ( content , 'base64' )
175- if ( isBinaryBuffer ( buf ) ) return null
176- return buf . toString ( 'utf8' )
179+ const buffer = await readBodyWithLimit ( response , MAX_FILE_SIZE )
180+ if ( ! buffer ) {
181+ throw new ConnectorFileTooLargeError ( MAX_FILE_SIZE )
177182 }
178- /**
179- * `GET /repos/{owner}/{repo}/git/blobs/{sha}` documents a single response
180- * encoding: "The `content` in the response will always be Base64 encoded."
181- * The "Currently, `utf-8` and `base64` are supported" sentence belongs to the
182- * `encoding` REQUEST parameter of `POST .../git/blobs` (Create a blob) and does
183- * not describe this response, so no `utf-8` branch is warranted here. Any other
184- * encoding would silently persist empty content, so it throws and surfaces as a
185- * failed document instead.
186- */
187- throw new Error ( `Unexpected git blob encoding for ${ sha } : ${ encoding ?? 'undefined' } ` )
183+ if ( isBinaryBuffer ( buffer ) ) return null
184+ return buffer . toString ( 'utf8' )
188185}
189186
190187/**
@@ -325,33 +322,14 @@ export const githubConnector: ConnectorConfig = {
325322 const response = await fetchWithRetry ( url , {
326323 method : 'GET' ,
327324 headers : {
328- Accept : 'application/vnd.github+json' ,
325+ Accept : 'application/vnd.github.object +json' ,
329326 Authorization : `Bearer ${ accessToken } ` ,
330327 'X-GitHub-Api-Version' : '2022-11-28' ,
331328 } ,
332329 } )
333330
334331 if ( ! response . ok ) {
335332 if ( response . status === 404 ) return null
336- /**
337- * A rate-limit 403 never reaches here: `fetchWithRetry` treats a 403 carrying
338- * `retry-after` or `x-ratelimit-remaining: 0` as retryable and throws once the
339- * retries are spent, so it lands in the catch below as a failure.
340- *
341- * A 403 that survives is usually an authorization denial, but NOT always: this
342- * request sends `application/vnd.github+json`, and the Contents API documents
343- * that files between 1-100 MB support "only the `raw` or `object` custom media
344- * types". A >1 MB text file therefore also lands here and is dropped, which on
345- * an `add` is silent (a fulfilled `null` records no failure). Reconciliation is
346- * unaffected — the file is already in `seenExternalIds` from the listing.
347- */
348- if ( response . status === 403 ) {
349- logger . info ( 'Skipping GitHub file rejected by Contents API' , {
350- path,
351- status : response . status ,
352- } )
353- return null
354- }
355333 throw new Error ( `Failed to fetch file ${ path } : ${ response . status } ` )
356334 }
357335
@@ -392,15 +370,18 @@ export const githubConnector: ConnectorConfig = {
392370 * "only the `raw` or `object` custom media types are supported", and it is
393371 * specifically "when using the `object` media type" that "the `content` field
394372 * will be an empty string and the `encoding` field will be `none`".
395- *
396- * This request sends `application/vnd.github+json`, so that precondition does
397- * not hold and this branch is currently unreachable — such files 403 above
398- * instead. Reaching it would require requesting
399- * `application/vnd.github.object+json`. The fallback itself is correct: the Git
400- * Blobs API returns the same blob as JSON and is documented to support blobs up
401- * to 100 MB.
373+ * The Git Blobs fallback streams that same blob through GitHub's documented raw
374+ * media type and supports blobs up to 100 MB.
402375 */
403- const blobContent = await fetchBlobContent ( accessToken , owner , repo , data . sha as string )
376+ let blobContent : string | null
377+ try {
378+ blobContent = await fetchBlobContent ( accessToken , owner , repo , data . sha as string )
379+ } catch ( error ) {
380+ if ( error instanceof ConnectorFileTooLargeError ) {
381+ return markSkipped ( stub , sizeLimitSkipReason ( MAX_FILE_SIZE ) )
382+ }
383+ throw error
384+ }
404385 if ( blobContent === null ) {
405386 logger . info ( 'Skipping binary GitHub file' , { path, size } )
406387 return markSkipped ( stub , BINARY_SKIP_REASON )
0 commit comments