Repository: ipfs/helia
Pull request #751: feat: allow modifying trustless-gateway fetch
Author: login: SgtPooki
Created: 2025-02-27T20:59:16Z | Merged: 2025-03-10T21:20:36Z

- **feat: add support for custom headers in trustless gateway**
- **chore: allow test trustless gateway server introspection**
- **feat: allow modification of fetch options to trustless gateway**

## Title

<!---
The title of the PR will be the commit message of the merge commit, so please make sure it is descriptive enough.
We utilize the Conventional Commits specification for our commit messages. See <https://www.conventionalcommits.org/en/v1.0.0/#specification> for more information.
The commit tag types can be of one of the following: feat, fix, deps, refactor, chore, docs. See <https://github.com/ipfs/helia/blob/main/.github/workflows/main.yml#L184-L192>
The title must also be fewer than 72 characters long or it will fail the Semantic PR check. See <https://github.com/ipfs/helia/blob/main/.github/workflows/semantic-pull-request.yml>
--->
feat: allow modifying trustless-gateway fetch

## Description

<!--
Please write a summary of your changes and why you made them.
Please include any relevant issues in here, for example:
Related https://github.com/ipfs/helia/issues/ABCD.
Fixes https://github.com/ipfs/helia/issues/XYZ.
-->

This PR allows consumers to modify headers and any other RequestInit options that trustless-gateways use when making a fetch request to a trustless gateway.

This allows consumers to add JWT tokens to the request, or modifying the request in a variety of other ways. If the user does not modify the requestInit object, the default behavior is to use the same requestInit object that was used prior to this PR.

Fixes #659
Replaces https://github.com/ipfs/helia/pull/732

## Notes & open questions

<!--
Any notes, remarks or open questions you have to make about the PR which don't need to go into the final commit message.
-->

## Change checklist

- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation if necessary (this includes comments as well)
- [x] I have added tests that prove my fix is effective or that my feature works


Linked issue #659: @helia/block-brokers should support http headers being passed
Hello,

I have been implementing helia into a new IPFS pinning service I will be launching EOY (paid service, but FOSS code) (uses the ipfs pinning spec). One barrier I ran into that I had to fork and create a pnpm patch is accessing the downloads on the trustless gateway behind a JWT.

I would like to request that my hack be used as a guide point for implementing support so I can stop needing to patch helia :P.

To be clear, I am currently offering a trustless gateway API, but it requires a JWT.

Kudos

```
Subject: [PATCH] wip
---
Index: packages/block-brokers/src/trustless-gateway/broker.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/src/trustless-gateway/broker.ts b/packages/block-brokers/src/trustless-gateway/broker.ts
--- a/packages/block-brokers/src/trustless-gateway/broker.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/src/trustless-gateway/broker.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -22,6 +22,8 @@
    * @default false
    */
   allowLocal?: boolean
+
+  headers?: Record<string, string>
 }
 
 /**
@@ -31,6 +33,7 @@
 export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
   private readonly allowInsecure: boolean
   private readonly allowLocal: boolean
+  private readonly headers?: Record<string, string>
   private readonly routing: Routing
   private readonly log: Logger
   private readonly logger: ComponentLogger
@@ -41,12 +44,13 @@
     this.routing = components.routing
     this.allowInsecure = init.allowInsecure ?? DEFAULT_ALLOW_INSECURE
     this.allowLocal = init.allowLocal ?? DEFAULT_ALLOW_LOCAL
+    this.headers = init.headers ?? {}
   }
 
   async retrieve (cid: CID, options: BlockRetrievalOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> {
     const aggregateErrors: Error[] = []
 
-    for await (const gateway of findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)) {
+    for await (const gateway of findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, this.headers,  options)) {
       this.log('getting block for %c from %s', cid, gateway.url)
 
       try {
@@ -93,7 +97,11 @@
     }, {
       ...options,
       allowLocal: this.allowLocal,
-      allowInsecure: this.allowInsecure
+      allowInsecure: this.allowInsecure,
+      headers: {
+        ...this.headers,
+        ...options.headers
+      }
     })
   }
 }
Index: packages/block-brokers/src/trustless-gateway/index.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/src/trustless-gateway/index.ts b/packages/block-brokers/src/trustless-gateway/index.ts
--- a/packages/block-brokers/src/trustless-gateway/index.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/src/trustless-gateway/index.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -25,6 +25,8 @@
    * @default false
    */
   allowLocal?: boolean
+
+  headers?: Record<string, string>
 }
 
 export interface TrustlessGatewayComponents {
Index: packages/block-brokers/src/trustless-gateway/session.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/src/trustless-gateway/session.ts b/packages/block-brokers/src/trustless-gateway/session.ts
--- a/packages/block-brokers/src/trustless-gateway/session.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/src/trustless-gateway/session.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -18,6 +18,7 @@
   private readonly routing: Routing
   private readonly allowInsecure: boolean
   private readonly allowLocal: boolean
+  private readonly headers?: Record<string, string>
 
   constructor (components: TrustlessGatewaySessionComponents, init: CreateTrustlessGatewaySessionOptions) {
     super(components, {
@@ -28,6 +29,7 @@
     this.routing = components.routing
     this.allowInsecure = init.allowInsecure ?? DEFAULT_ALLOW_INSECURE
     this.allowLocal = init.allowLocal ?? DEFAULT_ALLOW_LOCAL
+    this.headers = init.headers ?? {}
   }
 
   async queryProvider (cid: CID, provider: TrustlessGateway, options: BlockRetrievalOptions): Promise<Uint8Array> {
@@ -41,8 +43,8 @@
     return block
   }
 
-  async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
-    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)
+  async * findNewProviders (cid: CID, options: AbortOptions = {}, headers: Record<string, string> = {}): AsyncGenerator<TrustlessGateway> {
+    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, this.headers, options)
   }
 
   toEvictionKey (provider: TrustlessGateway): Uint8Array | string {
Index: packages/block-brokers/src/trustless-gateway/trustless-gateway.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts b/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts
--- a/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -54,9 +54,11 @@
   readonly #pendingResponses = new Map<string, Promise<Uint8Array>>()
 
   private readonly log: Logger
+  private readonly headers: Record<string, string>
 
-  constructor (url: URL | string, logger: ComponentLogger) {
+  constructor (url: URL | string, headers: Record<string, string>, logger: ComponentLogger) {
     this.url = url instanceof URL ? url : new URL(url)
+    this.headers = headers
     this.log = logger.forComponent(`helia:trustless-gateway-block-broker:${this.url.hostname}`)
   }
 
@@ -106,7 +108,8 @@
         pendingResponse = fetch(gwUrl.toString(), {
           signal: innerController.signal,
           headers: {
-            Accept: 'application/vnd.ipld.raw'
+            Accept: 'application/vnd.ipld.raw',
+            ...this.headers
           },
           cache: 'force-cache'
         }).then(async (res) => {
Index: packages/block-brokers/src/trustless-gateway/utils.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/src/trustless-gateway/utils.ts b/packages/block-brokers/src/trustless-gateway/utils.ts
--- a/packages/block-brokers/src/trustless-gateway/utils.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/src/trustless-gateway/utils.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -33,7 +33,7 @@
   })
 }
 
-export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, options?: AbortOptions): AsyncGenerator<TrustlessGateway> {
+export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, headers: Record<string, string> = {}, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
   for await (const provider of routing.findProviders(cid, options)) {
     // require http(s) addresses
     const httpAddresses = filterNonHTTPMultiaddrs(provider.multiaddrs, allowInsecure, allowLocal)
@@ -48,6 +48,6 @@
     // etc
     const uri = multiaddrToUri(httpAddresses[0])
 
-    yield new TrustlessGateway(uri, logger)
+    yield new TrustlessGateway(uri, headers, logger)
   }
 }
Index: packages/block-brokers/test/trustless-gateway.spec.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/packages/block-brokers/test/trustless-gateway.spec.ts b/packages/block-brokers/test/trustless-gateway.spec.ts
--- a/packages/block-brokers/test/trustless-gateway.spec.ts	(revision ac4bdb8a73cab23500221340830969552a1d8db6)
+++ b/packages/block-brokers/test/trustless-gateway.spec.ts	(revision 1955f39d9f32960978d66d9782282acb4478b3f4)
@@ -148,7 +148,7 @@
     if (process.env.TRUSTLESS_GATEWAY == null) {
       return this.skip()
     }
-    const trustlessGateway = new TrustlessGateway(process.env.TRUSTLESS_GATEWAY, defaultLogger())
+    const trustlessGateway = new TrustlessGateway(process.env.TRUSTLESS_GATEWAY, {}, defaultLogger())
 
     // Call getRawBlock multiple times with the same CID
     const promises = Array.from({ length: 10 }, async () => trustlessGateway.getRawBlock(cid))
```
Linked issue comments: user: login: pcfreak30
created_at: 2025-01-21T18:39:31Z
body: @SgtPooki I would appreciate a follow up on this as I would really like to not need to patch the code.

Kudos.
author_association: CONTRIBUTOR
user: login: SgtPooki
created_at: 2025-01-29T21:47:21Z
body: @pcfreak30 I think we are willing to fix BlockBrokers so we can customize headers, but it would help if you could post the diff/patch as a PR we can look at so we don't have to review a codeblock in an issue description
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-01-30T00:32:10Z
body: > [@pcfreak30](https://github.com/pcfreak30) I think we are willing to fix BlockBrokers so we can customize headers, but it would help if you could post the diff/patch as a PR we can look at so we don't have to review a codeblock in an issue description

Ok, I didnt want to do a PR as my way was prob a hack, and I wanted to just provide a guide point for you to create your own impl.

But if you do want it, I can create a draft PR at-least.
author_association: CONTRIBUTOR
user: login: pcfreak30
created_at: 2025-02-01T09:52:13Z
body: @SgtPooki see https://github.com/ipfs/helia/pull/732
author_association: CONTRIBUTOR
user: login: pcfreak30
created_at: 2025-02-27T16:10:07Z
body: @SgtPooki following up.
author_association: CONTRIBUTOR
user: login: SgtPooki
created_at: 2025-02-27T16:15:32Z
body: @pcfreak30 thanks for throwing that together. We should have talked about this at the Helia WG meeting that just happened.. 
author_association: MEMBER

Diff:
diff --git a/packages/block-brokers/.aegir.js b/packages/block-brokers/.aegir.js
index 161be0a71..93c6b9f68 100644
--- a/packages/block-brokers/.aegir.js
+++ b/packages/block-brokers/.aegir.js
@@ -1,6 +1,23 @@
 import cors from 'cors'
 import polka from 'polka'
 
+
+/**
+ * Middleware to log requests
+ */
+const requestLogs = []
+let enableLogs = false
+function logRequests(req, res, next) {
+  if (!req.url.includes('/logs') && enableLogs) {
+    requestLogs.push({
+      method: req.method,
+      url: req.url,
+      headers: req.headers
+    })
+  }
+  next()
+}
+
 /** @type {import('aegir').PartialOptions} */
 const options = {
   test: {
@@ -10,6 +27,7 @@ const options = {
         host: '127.0.0.1'
       })
       goodGateway.use(cors())
+      goodGateway.use(logRequests)
       goodGateway.all('/ipfs/bafkreiefnkxuhnq3536qo2i2w3tazvifek4mbbzb6zlq3ouhprjce5c3aq', (req, res) => {
         res.writeHead(200, {
           'content-type': 'application/octet-stream',
@@ -28,6 +46,31 @@ const options = {
         // "hello"
         res.end(Uint8Array.from([104, 101, 108, 108, 111]))
       })
+      goodGateway.all('/ipfs/*', (req, res) => {
+        // succeeds with empty block for any other CID
+        res.writeHead(200)
+        res.end(Uint8Array.from([]))
+      })
+
+      goodGateway.all('/logs', (req, res) => {
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify(requestLogs))
+      })
+      goodGateway.all('/logs/enable', (req, res) => {
+        enableLogs = true
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logging enabled' }))
+      })
+      goodGateway.all('/logs/disable', (req, res) => {
+        enableLogs = false
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logging disabled' }))
+      })
+      goodGateway.all('/logs/clear', (req, res) => {
+        requestLogs.length = 0
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logs cleared' }))
+      })
 
       await goodGateway.listen()
       const { port: goodGatewayPort } = goodGateway.server.address()
diff --git a/packages/block-brokers/src/trustless-gateway/broker.ts b/packages/block-brokers/src/trustless-gateway/broker.ts
index 05d2b518a..db08691c2 100644
--- a/packages/block-brokers/src/trustless-gateway/broker.ts
+++ b/packages/block-brokers/src/trustless-gateway/broker.ts
@@ -2,6 +2,7 @@ import { createTrustlessGatewaySession } from './session.js'
 import { findHttpGatewayProviders } from './utils.js'
 import { DEFAULT_ALLOW_INSECURE, DEFAULT_ALLOW_LOCAL } from './index.js'
 import type { TrustlessGatewayBlockBrokerInit, TrustlessGatewayBlockBrokerComponents, TrustlessGatewayGetBlockProgressEvents } from './index.js'
+import type { TransformRequestInit } from './trustless-gateway.js'
 import type { Routing, BlockRetrievalOptions, BlockBroker, CreateSessionOptions } from '@helia/interface'
 import type { ComponentLogger, Logger } from '@libp2p/interface'
 import type { CID } from 'multiformats/cid'
@@ -22,6 +23,10 @@ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptio
    * @default false
    */
   allowLocal?: boolean
+  /**
+   * Provide a function that will be called before querying trustless-gateways. This lets you modify the fetch options to pass custom headers or other necessary things.
+   */
+  transformRequestInit?: TransformRequestInit
 }
 
 /**
@@ -31,6 +36,7 @@ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptio
 export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
   private readonly allowInsecure: boolean
   private readonly allowLocal: boolean
+  private readonly transformRequestInit?: TransformRequestInit
   private readonly routing: Routing
   private readonly log: Logger
   private readonly logger: ComponentLogger
@@ -41,12 +47,13 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
     this.routing = components.routing
     this.allowInsecure = init.allowInsecure ?? DEFAULT_ALLOW_INSECURE
     this.allowLocal = init.allowLocal ?? DEFAULT_ALLOW_LOCAL
+    this.transformRequestInit = init.transformRequestInit
   }
 
   async retrieve (cid: CID, options: BlockRetrievalOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> {
     const aggregateErrors: Error[] = []
 
-    for await (const gateway of findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)) {
+    for await (const gateway of findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, { ...options, transformRequestInit: this.transformRequestInit })) {
       this.log('getting block for %c from %s', cid, gateway.url)
 
       try {
@@ -93,7 +100,8 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
     }, {
       ...options,
       allowLocal: this.allowLocal,
-      allowInsecure: this.allowInsecure
+      allowInsecure: this.allowInsecure,
+      transformRequestInit: this.transformRequestInit
     })
   }
 }
diff --git a/packages/block-brokers/src/trustless-gateway/index.ts b/packages/block-brokers/src/trustless-gateway/index.ts
index 9cda5788b..9aab42bae 100644
--- a/packages/block-brokers/src/trustless-gateway/index.ts
+++ b/packages/block-brokers/src/trustless-gateway/index.ts
@@ -1,4 +1,5 @@
 import { TrustlessGatewayBlockBroker } from './broker.js'
+import type { TransformRequestInit } from './trustless-gateway.js'
 import type { Routing, BlockBroker } from '@helia/interface'
 import type { ComponentLogger } from '@libp2p/interface'
 import type { ProgressEvent } from 'progress-events'
@@ -25,6 +26,10 @@ export interface TrustlessGatewayBlockBrokerInit {
    * @default false
    */
   allowLocal?: boolean
+  /**
+   * Provide a function that will be called before querying trustless-gateways. This lets you modify the fetch options to pass custom headers or other necessary things.
+   */
+  transformRequestInit?: TransformRequestInit
 }
 
 export interface TrustlessGatewayBlockBrokerComponents {
diff --git a/packages/block-brokers/src/trustless-gateway/session.ts b/packages/block-brokers/src/trustless-gateway/session.ts
index 9d26d7435..711d980ae 100644
--- a/packages/block-brokers/src/trustless-gateway/session.ts
+++ b/packages/block-brokers/src/trustless-gateway/session.ts
@@ -3,7 +3,7 @@ import { findHttpGatewayProviders } from './utils.js'
 import { DEFAULT_ALLOW_INSECURE, DEFAULT_ALLOW_LOCAL } from './index.js'
 import type { CreateTrustlessGatewaySessionOptions } from './broker.js'
 import type { TrustlessGatewayGetBlockProgressEvents } from './index.js'
-import type { TrustlessGateway } from './trustless-gateway.js'
+import type { TransformRequestInit, TrustlessGateway } from './trustless-gateway.js'
 import type { BlockRetrievalOptions, Routing } from '@helia/interface'
 import type { ComponentLogger } from '@libp2p/interface'
 import type { AbortOptions } from 'interface-store'
@@ -18,6 +18,7 @@ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, Trustles
   private readonly routing: Routing
   private readonly allowInsecure: boolean
   private readonly allowLocal: boolean
+  private readonly transformRequestInit?: TransformRequestInit
 
   constructor (components: TrustlessGatewaySessionComponents, init: CreateTrustlessGatewaySessionOptions) {
     super(components, {
@@ -28,6 +29,7 @@ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, Trustles
     this.routing = components.routing
     this.allowInsecure = init.allowInsecure ?? DEFAULT_ALLOW_INSECURE
     this.allowLocal = init.allowLocal ?? DEFAULT_ALLOW_LOCAL
+    this.transformRequestInit = init.transformRequestInit
   }
 
   async queryProvider (cid: CID, provider: TrustlessGateway, options: BlockRetrievalOptions): Promise<Uint8Array> {
@@ -42,7 +44,7 @@ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, Trustles
   }
 
   async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
-    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)
+    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, { ...options, transformRequestInit: this.transformRequestInit })
   }
 
   toEvictionKey (provider: TrustlessGateway): Uint8Array | string {
diff --git a/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts b/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts
index 97a5faef5..db6afac07 100644
--- a/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts
+++ b/packages/block-brokers/src/trustless-gateway/trustless-gateway.ts
@@ -10,6 +10,15 @@ export interface TrustlessGatewayStats {
   pendingResponses?: number
 }
 
+export interface TransformRequestInit {
+  (defaultReqInit: RequestInit): Promise<RequestInit> | RequestInit
+}
+
+export interface TrustlessGatewayComponents {
+  logger: ComponentLogger
+  transformRequestInit?: TransformRequestInit
+}
+
 /**
  * A `TrustlessGateway` keeps track of the number of attempts, errors, and
  * successes for a given gateway url so that we can prioritize gateways that
@@ -54,9 +63,11 @@ export class TrustlessGateway {
   readonly #pendingResponses = new Map<string, Promise<Uint8Array>>()
 
   private readonly log: Logger
+  private readonly transformRequestInit?: TransformRequestInit
 
-  constructor (url: URL | string, logger: ComponentLogger) {
+  constructor (url: URL | string, { logger, transformRequestInit }: TrustlessGatewayComponents) {
     this.url = url instanceof URL ? url : new URL(url)
+    this.transformRequestInit = transformRequestInit
     this.log = logger.forComponent(`helia:trustless-gateway-block-broker:${this.url.hostname}`)
   }
 
@@ -103,13 +114,17 @@ export class TrustlessGateway {
       let pendingResponse: Promise<Uint8Array> | undefined = this.#pendingResponses.get(blockId)
       if (pendingResponse == null) {
         this.#attempts++
-        pendingResponse = fetch(gwUrl.toString(), {
+        const defaultReqInit: RequestInit = {
           signal: innerController.signal,
           headers: {
             Accept: 'application/vnd.ipld.raw'
           },
           cache: 'force-cache'
-        }).then(async (res) => {
+        }
+
+        const reqInit: RequestInit = this.transformRequestInit != null ? await this.transformRequestInit(defaultReqInit) : defaultReqInit
+
+        pendingResponse = fetch(gwUrl.toString(), reqInit).then(async (res) => {
           this.log('GET %s %d', gwUrl, res.status)
           if (!res.ok) {
             this.#errors++
diff --git a/packages/block-brokers/src/trustless-gateway/utils.ts b/packages/block-brokers/src/trustless-gateway/utils.ts
index 820c44ff1..6924ede1f 100644
--- a/packages/block-brokers/src/trustless-gateway/utils.ts
+++ b/packages/block-brokers/src/trustless-gateway/utils.ts
@@ -1,7 +1,7 @@
 import { isPrivateIp } from '@libp2p/utils/private-ip'
 import { DNS, HTTP, HTTPS } from '@multiformats/multiaddr-matcher'
 import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
-import { TrustlessGateway } from './trustless-gateway.js'
+import { TrustlessGateway, type TransformRequestInit } from './trustless-gateway.js'
 import type { Routing } from '@helia/interface'
 import type { ComponentLogger } from '@libp2p/interface'
 import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
@@ -33,7 +33,11 @@ export function filterNonHTTPMultiaddrs (multiaddrs: Multiaddr[], allowInsecure:
   })
 }
 
-export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, options?: AbortOptions): AsyncGenerator<TrustlessGateway> {
+export interface FindHttpGatewayProvidersOptions extends AbortOptions {
+  transformRequestInit?: TransformRequestInit
+}
+
+export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, options: FindHttpGatewayProvidersOptions = {}): AsyncGenerator<TrustlessGateway> {
   for await (const provider of routing.findProviders(cid, options)) {
     // require http(s) addresses
     const httpAddresses = filterNonHTTPMultiaddrs(provider.multiaddrs, allowInsecure, allowLocal)
@@ -48,6 +52,6 @@ export async function * findHttpGatewayProviders (cid: CID, routing: Routing, lo
     // etc
     const uri = multiaddrToUri(httpAddresses[0])
 
-    yield new TrustlessGateway(uri, logger)
+    yield new TrustlessGateway(uri, { logger, transformRequestInit: options.transformRequestInit })
   }
 }
diff --git a/packages/block-brokers/test/trustless-gateway.spec.ts b/packages/block-brokers/test/trustless-gateway.spec.ts
index 8b9496632..8a43c683c 100644
--- a/packages/block-brokers/test/trustless-gateway.spec.ts
+++ b/packages/block-brokers/test/trustless-gateway.spec.ts
@@ -149,7 +149,7 @@ describe('trustless-gateway-block-broker', () => {
     if (process.env.TRUSTLESS_GATEWAY == null) {
       return this.skip()
     }
-    const trustlessGateway = new TrustlessGateway(process.env.TRUSTLESS_GATEWAY, defaultLogger())
+    const trustlessGateway = new TrustlessGateway(process.env.TRUSTLESS_GATEWAY, { logger: defaultLogger() })
 
     // Call getRawBlock multiple times with the same CID
     const promises = Array.from({ length: 10 }, async () => trustlessGateway.getRawBlock(cid))
@@ -171,4 +171,35 @@ describe('trustless-gateway-block-broker', () => {
       pendingResponses: 0 // the queue is empty
     })
   })
+
+  it('can pass custom headers to the gateway', async function () {
+    if (process.env.TRUSTLESS_GATEWAY == null) {
+      return this.skip()
+    }
+    const cid = CID.parse('bafybeic3q4y65yxu3yckr76q63bcvanhklwf6cwxuacnrot6v3gykrgsvq')
+
+    const trustlessGateway = new TrustlessGateway(process.env.TRUSTLESS_GATEWAY, {
+      logger: defaultLogger(),
+      transformRequestInit: (requestInit) => {
+        requestInit.headers = {
+          ...requestInit.headers,
+          'X-My-Header': 'my-value'
+        }
+
+        return requestInit
+      }
+    })
+
+    await fetch(`${process.env.TRUSTLESS_GATEWAY}/logs/enable`)
+    await trustlessGateway.getRawBlock(cid)
+    await fetch(`${process.env.TRUSTLESS_GATEWAY}/logs/disable`)
+
+    const reqLogs = await fetch(`${process.env.TRUSTLESS_GATEWAY}/logs`)
+    const logs = await reqLogs.json()
+    await fetch(`${process.env.TRUSTLESS_GATEWAY}/logs/clear`)
+
+    // assert that fetch was called with the custom header
+    expect(logs).to.have.lengthOf(1)
+    expect(logs[0].headers['x-my-header']).to.equal('my-value')
+  })
 })


Reviews:
user: login: SgtPooki
body: self review
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T21:03:20Z
user: login: 2color
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T20:37:56Z
user: login: 2color
body: Nice! LGTM. 

I'd add an example to the helia README (not a blocker. we can do it in a followup)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-04T20:43:11Z
user: login: achingbrain
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T13:03:22Z
user: login: achingbrain
body: LGTM.  Small nit about the optional arg.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T13:29:13Z
user: login: SgtPooki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T19:44:06Z

Review comments:
diff_hunk: @@ -29,7 +47,28 @@ const options = {
         res.end(Uint8Array.from([104, 101, 108, 108, 111]))
       })
 
+      goodGateway.all('/logs', (req, res) => {
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify(requestLogs))
+      })
+      goodGateway.all('/logs/enable', (req, res) => {
+        enableLogs = true
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logging enabled' }))
+      })
+      goodGateway.all('/logs/disable', (req, res) => {
+        enableLogs = false
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logging disabled' }))
+      })
+      goodGateway.all('/logs/clear', (req, res) => {
+        requestLogs.length = 0
+        res.writeHead(200, { 'content-type': 'application/json' })
+        res.end(JSON.stringify({ message: 'Logs cleared' }))
+      })
+
       await goodGateway.listen()
+
path: packages/block-brokers/.aegir.js
user: login: SgtPooki
body: ```suggestion
```
created_at: 2025-02-27T21:00:03Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,8 @@ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptio
    * @default false
    */
   allowLocal?: boolean
+
+  transformRequestInit?: TransformRequestInit
path: packages/block-brokers/src/trustless-gateway/broker.ts
user: login: SgtPooki
body: ```suggestion
  /**
   * Provide a function that will be called before querying trustless-gateways. This lets you modify the fetch options to pass custom headers or other necessary things.
   */
  transformRequestInit?: TransformRequestInit
```
created_at: 2025-02-27T21:01:48Z
author_association: MEMBER
diff_hunk: @@ -25,6 +26,8 @@ export interface TrustlessGatewayBlockBrokerInit {
    * @default false
    */
   allowLocal?: boolean
+
+  transformRequestInit?: TransformRequestInit
path: packages/block-brokers/src/trustless-gateway/index.ts
user: login: SgtPooki
body: ```suggestion
  /**
   * Provide a function that will be called before querying trustless-gateways. This lets you modify the fetch options to pass custom headers or other necessary things.
   */
  transformRequestInit?: TransformRequestInit
```
created_at: 2025-02-27T21:02:24Z
author_association: MEMBER
diff_hunk: @@ -41,8 +43,8 @@ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, Trustles
     return block
   }
 
-  async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
-    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)
+  async * findNewProviders (cid: CID, options: AbortOptions = {}, headers: Record<string, string> = {}): AsyncGenerator<TrustlessGateway> {
path: packages/block-brokers/src/trustless-gateway/session.ts
user: login: 2color
body: Do we still need `headers` here? It doesn't seem like we're doing anything with it
```suggestion
  async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
```
created_at: 2025-03-04T20:37:56Z
author_association: MEMBER
diff_hunk: @@ -33,7 +33,7 @@ export function filterNonHTTPMultiaddrs (multiaddrs: Multiaddr[], allowInsecure:
   })
 }
 
-export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, options?: AbortOptions): AsyncGenerator<TrustlessGateway> {
+export async function * findHttpGatewayProviders (cid: CID, routing: Routing, logger: ComponentLogger, allowInsecure: boolean, allowLocal: boolean, transformRequestInit?: TransformRequestInit, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
path: packages/block-brokers/src/trustless-gateway/utils.ts
user: login: achingbrain
body: Given that it's optional, can `transformRequestInit` go in the options object?
created_at: 2025-03-06T13:03:22Z
author_association: MEMBER
diff_hunk: @@ -41,8 +43,8 @@ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, Trustles
     return block
   }
 
-  async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
-    yield * findHttpGatewayProviders(cid, this.routing, this.logger, this.allowInsecure, this.allowLocal, options)
+  async * findNewProviders (cid: CID, options: AbortOptions = {}, headers: Record<string, string> = {}): AsyncGenerator<TrustlessGateway> {
path: packages/block-brokers/src/trustless-gateway/session.ts
user: login: SgtPooki
body: removed
created_at: 2025-03-10T19:44:06Z
author_association: MEMBER

PR comments:
user: login: SgtPooki
created_at: 2025-02-27T21:04:57Z
body: @pcfreak30 can you confirm this will work for your use-case?

cc @achingbrain 
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-02-27T21:40:16Z
body: > @pcfreak30 can you confirm this will work for your use-case?
> 
> cc @achingbrain

At a glance, yes. primary use case is a JWT.
author_association: CONTRIBUTOR
user: login: SgtPooki
created_at: 2025-02-27T21:56:00Z
body: I'll need to look further into the chrome failures later. I was able to reproduce these locally ~but cannot any longer~.

fixed with [`cd1f716` (#751)](https://github.com/ipfs/helia/pull/751/commits/cd1f716593624ae986089dfcd94f3add513d850e)
author_association: MEMBER
user: login: SgtPooki
created_at: 2025-03-04T20:32:38Z
body: ping @achingbrain and @2color for review
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-03-04T21:11:17Z
body: Happy to see this get merged in :).

Thanks for collaborating.
author_association: CONTRIBUTOR