Skip to content

Commit 6c1e66f

Browse files
authored
fix: simplify plugin pipeline (#282)
Plugins can return a response or throw an error, in which case we exit or return `null` in which case we continue. If the plugin needs to return an error response it can do that after doing any logging it needs to do.
1 parent 9582705 commit 6c1e66f

20 files changed

Lines changed: 225 additions & 227 deletions

packages/verified-fetch/README.md

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -672,13 +672,13 @@ Each plugin must implement two methods:
672672
Inspects the current `PluginContext` (which includes the CID, path, query, accept header, etc.)
673673
and returns `true` if the plugin can operate on the current state of the request.
674674

675-
- **`handle(context: PluginContext): Promise<Response | null>`**
676-
Performs the plugin’s work. It may:
677-
- **Return a final `Response`**: This stops the pipeline immediately.
678-
- **Return `null`**: This indicates that the plugin has only partially processed the request
675+
- **`handle(context: PluginContext): Promise<Response | undefined>`**
676+
Performs the plugin’s work. It will only be executed if `canHandle` previously returned `true`.
677+
It may:
678+
- **Return a `Response`**: This stops the pipeline immediately and returns the response.
679+
- **Return `undefined`**: This indicates that the plugin has only partially processed the request
679680
(for example, by performing path walking or decoding) and the pipeline should continue.
680-
- **Throw a `PluginError`**: This logs a non-fatal error and continues the pipeline.
681-
- **Throw a `PluginFatalError`**: This logs a fatal error and stops the pipeline immediately.
681+
- **Throw an `Error`**: An Internal Server Error will be returned
682682

683683
### Plugin Pipeline
684684

@@ -816,47 +816,6 @@ To add your own plugin:
816816
const fetch = await createVerifiedFetch(helia, { plugins })
817817
```
818818

819-
***
820-
821-
### Error Handling in the Plugin Pipeline
822-
823-
Verified‑Fetch distinguishes between two types of errors thrown by plugins:
824-
825-
- **PluginError (Non‑Fatal):**
826-
- Use this when your plugin encounters an issue that should be logged but does not prevent the pipeline
827-
from continuing.
828-
- When a plugin throws a `PluginError`, the error is logged and the pipeline continues with the next plugin.
829-
830-
- **PluginFatalError (Fatal):**
831-
- Use this when a critical error occurs that should immediately abort the request.
832-
- When a plugin throws a `PluginFatalError`, the pipeline immediately terminates and the provided error
833-
response is returned.
834-
835-
## Example - Plugin error Handling
836-
837-
```typescript
838-
import { PluginError, PluginFatalError } from '@helia/verified-fetch'
839-
840-
// async handle(context: PluginContext): Promise<Response | null> {
841-
const recoverable = Math.random() > 0.5 // Use more sophisticated logic here ;)
842-
if (recoverable === true) {
843-
throw new PluginError('MY_CUSTOM_WARNING', 'A non‑fatal issue occurred', {
844-
details: {
845-
someKey: 'Additional details here'
846-
}
847-
})
848-
}
849-
850-
if (recoverable === false) {
851-
throw new PluginFatalError('MY_CUSTOM_FATAL', 'A critical error occurred', {
852-
response: new Response('Something happened', { status: 500 }) // Required: supply your own error response
853-
})
854-
}
855-
856-
// Otherwise, continue processing...
857-
// }
858-
```
859-
860819
### How the Plugin Pipeline Works
861820

862821
- **Shared Context:**
@@ -874,8 +833,7 @@ if (recoverable === false) {
874833
This means you do not have to specify a rigid order, each plugin simply checks the context and acts if appropriate.
875834

876835
- **Error Handling:**
877-
- A thrown `PluginError` is considered non‑fatal and is logged, allowing the pipeline to continue.
878-
- A thrown `PluginFatalError` immediately stops the pipeline and returns the error response.
836+
- Any thrown error immediately stops the pipeline and returns the error response.
879837

880838
For a detailed explanation of the pipeline, please refer to the discussion in [Issue #167](https://github.com/ipfs/helia-verified-fetch/issues/167).
881839

packages/verified-fetch/src/index.ts

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -641,13 +641,13 @@
641641
* Inspects the current `PluginContext` (which includes the CID, path, query, accept header, etc.)
642642
* and returns `true` if the plugin can operate on the current state of the request.
643643
*
644-
* - **`handle(context: PluginContext): Promise<Response | null>`**
645-
* Performs the plugin’s work. It may:
646-
* - **Return a final `Response`**: This stops the pipeline immediately.
647-
* - **Return `null`**: This indicates that the plugin has only partially processed the request
648-
* (for example, by performing path walking or decoding) and the pipeline should continue.
649-
* - **Throw a `PluginError`**: This logs a non-fatal error and continues the pipeline.
650-
* - **Throw a `PluginFatalError`**: This logs a fatal error and stops the pipeline immediately.
644+
* - **`handle(context: PluginContext): Promise<Response | undefined>`**
645+
* Performs the plugin’s work. It will only be executed if `canHandle` previously returned `true`.
646+
* It may:
647+
* - **Return a `Response`**: This stops the pipeline immediately and returns the response.
648+
* - **Return `undefined`**: This indicates that the plugin has only partially processed the request
649+
* (for example, by performing path walking or decoding) and the pipeline should continue.
650+
* - **Throw an `Error`**: An Internal Server Error will be returned
651651
*
652652
* ### Plugin Pipeline
653653
*
@@ -784,47 +784,6 @@
784784
* const fetch = await createVerifiedFetch(helia, { plugins })
785785
* ```
786786
*
787-
* ---
788-
*
789-
* ### Error Handling in the Plugin Pipeline
790-
*
791-
* Verified‑Fetch distinguishes between two types of errors thrown by plugins:
792-
*
793-
* - **PluginError (Non‑Fatal):**
794-
* - Use this when your plugin encounters an issue that should be logged but does not prevent the pipeline
795-
* from continuing.
796-
* - When a plugin throws a `PluginError`, the error is logged and the pipeline continues with the next plugin.
797-
*
798-
* - **PluginFatalError (Fatal):**
799-
* - Use this when a critical error occurs that should immediately abort the request.
800-
* - When a plugin throws a `PluginFatalError`, the pipeline immediately terminates and the provided error
801-
* response is returned.
802-
*
803-
* @example Plugin error Handling
804-
*
805-
* ```typescript
806-
* import { PluginError, PluginFatalError } from '@helia/verified-fetch'
807-
*
808-
* // async handle(context: PluginContext): Promise<Response | null> {
809-
* const recoverable = Math.random() > 0.5 // Use more sophisticated logic here ;)
810-
* if (recoverable === true) {
811-
* throw new PluginError('MY_CUSTOM_WARNING', 'A non‑fatal issue occurred', {
812-
* details: {
813-
* someKey: 'Additional details here'
814-
* }
815-
* })
816-
* }
817-
*
818-
* if (recoverable === false) {
819-
* throw new PluginFatalError('MY_CUSTOM_FATAL', 'A critical error occurred', {
820-
* response: new Response('Something happened', { status: 500 }) // Required: supply your own error response
821-
* })
822-
* }
823-
*
824-
* // Otherwise, continue processing...
825-
* // }
826-
* ```
827-
*
828787
* ### How the Plugin Pipeline Works
829788
*
830789
* - **Shared Context:**
@@ -842,8 +801,7 @@
842801
* This means you do not have to specify a rigid order, each plugin simply checks the context and acts if appropriate.
843802
*
844803
* - **Error Handling:**
845-
* - A thrown `PluginError` is considered non‑fatal and is logged, allowing the pipeline to continue.
846-
* - A thrown `PluginFatalError` immediately stops the pipeline and returns the error response.
804+
* - Any thrown error immediately stops the pipeline and returns the error response.
847805
*
848806
* For a detailed explanation of the pipeline, please refer to the discussion in [Issue #167](https://github.com/ipfs/helia-verified-fetch/issues/167).
849807
*/

packages/verified-fetch/src/plugins/errors.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

packages/verified-fetch/src/plugins/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* This file is the entry into all things we export from the `src/plugins` directory.
33
*/
44

5-
export { PluginError, PluginFatalError } from './errors.js'
65
export { BasePlugin } from './plugin-base.js'
76
export type { PluginOptions, PluginContext, VerifiedFetchPluginFactory } from './types.js'
87
export * from './plugins.js'

packages/verified-fetch/src/plugins/plugin-handle-car.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ export class CarPlugin extends BasePlugin {
4747
readonly id = 'car-plugin'
4848

4949
canHandle (context: PluginContext): boolean {
50-
this.log('checking if we can handle %c with accept %s', context.cid, context.accept)
51-
5250
if (context.byteRangeContext == null) {
5351
return false
5452
}

packages/verified-fetch/src/plugins/plugin-handle-dag-cbor-html-preview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ export class DagCborHtmlPreviewPlugin extends BasePlugin {
5151
readonly codes = [ipldDagCbor.code]
5252

5353
canHandle ({ cid, accept, pathDetails }: PluginContext): boolean {
54-
this.log('checking if we can handle %c with accept %s', cid, accept)
5554
if (pathDetails == null) {
5655
return false
5756
}
57+
5858
if (!isObjectNode(pathDetails.terminalElement)) {
5959
return false
6060
}
61+
6162
if (cid.code !== ipldDagCbor.code) {
6263
return false
6364
}

packages/verified-fetch/src/plugins/plugin-handle-dag-cbor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ export class DagCborPlugin extends BasePlugin {
1616
readonly codes = [ipldDagCbor.code]
1717

1818
canHandle ({ cid, accept, pathDetails, byteRangeContext, plugins }: PluginContext): boolean {
19-
this.log('checking if we can handle %c with accept %s', cid, accept)
2019
if (pathDetails == null) {
2120
return false
2221
}
22+
2323
if (!isObjectNode(pathDetails.terminalElement)) {
2424
return false
2525
}
26+
2627
if (cid.code !== ipldDagCbor.code) {
2728
return false
2829
}
30+
2931
if (byteRangeContext == null) {
3032
return false
3133
}

packages/verified-fetch/src/plugins/plugin-handle-dag-pb.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CustomProgressEvent } from 'progress-events'
66
import { getContentType } from '../utils/get-content-type.js'
77
import { getStreamFromAsyncIterable } from '../utils/get-stream-from-async-iterable.js'
88
import { setIpfsRoots } from '../utils/response-headers.js'
9-
import { badGatewayResponse, badRangeResponse, movedPermanentlyResponse, notSupportedResponse, okRangeResponse } from '../utils/responses.js'
9+
import { badGatewayResponse, badRangeResponse, movedPermanentlyResponse, okRangeResponse } from '../utils/responses.js'
1010
import { BasePlugin } from './plugin-base.js'
1111
import type { PluginContext } from './types.js'
1212
import type { CIDDetail } from '../index.js'
@@ -18,15 +18,21 @@ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
1818
export class DagPbPlugin extends BasePlugin {
1919
readonly id = 'dag-pb-plugin'
2020
readonly codes = [dagPbCode]
21+
2122
canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
22-
this.log('checking if we can handle %c with accept %s', cid, accept)
2323
if (pathDetails == null) {
2424
return false
2525
}
26+
2627
if (byteRangeContext == null) {
2728
return false
2829
}
2930

31+
// TODO: this may be too restrictive?
32+
if (accept != null && accept.mimeType !== 'application/octet-stream') {
33+
return false
34+
}
35+
3036
return cid.code === dagPbCode
3137
}
3238

@@ -105,21 +111,20 @@ export class DagPbPlugin extends BasePlugin {
105111
if (options?.signal?.aborted) {
106112
throw new AbortError(options?.signal?.reason)
107113
}
108-
this.log.error('error loading path %c/%s', dirCid, rootFilePath, err)
114+
115+
this.log.error('error loading path %c/%s - %e', dirCid, rootFilePath, err)
116+
109117
context.isDirectory = true
110118
context.directoryEntries = []
111119
context.modified++
120+
112121
this.log.trace('attempting to get directory entries because index.html was not found')
113-
try {
114-
for await (const dirItem of fs.ls(dirCid, { signal: options?.signal, onProgress: options?.onProgress, extended: false })) {
115-
context.directoryEntries.push(dirItem)
116-
}
117-
// dir-index-html plugin or dir-index-json (future idea?) plugin should handle this
118-
return null
119-
} catch (e) {
120-
log.error('error listing directory %c', dirCid, e)
121-
return notSupportedResponse('Unable to get directory contents')
122+
for await (const dirItem of fs.ls(dirCid, { signal: options?.signal, onProgress: options?.onProgress, extended: false })) {
123+
context.directoryEntries.push(dirItem)
122124
}
125+
126+
// dir-index-html plugin or dir-index-json (future idea?) plugin should handle this
127+
return null
123128
} finally {
124129
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
125130
}
@@ -152,7 +157,7 @@ export class DagPbPlugin extends BasePlugin {
152157
})
153158
log('got async iterator for %c/%s', cid, path)
154159

155-
const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, path ?? '', this.pluginOptions.logger, {
160+
const streamAndFirstChunk = await context.serverTiming.time('stream-and-chunk', '', getStreamFromAsyncIterable(asyncIter, path, this.pluginOptions.logger, {
156161
onProgress: options?.onProgress,
157162
signal: options?.signal
158163
}))
@@ -177,11 +182,14 @@ export class DagPbPlugin extends BasePlugin {
177182
if (options?.signal?.aborted) {
178183
throw new AbortError(options?.signal?.reason)
179184
}
180-
log.error('error streaming %c/%s', cid, path, err)
185+
186+
log.error('error streaming %c/%s - %e', cid, path, err)
187+
181188
if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
182189
return badRangeResponse(resource)
183190
}
184-
return badGatewayResponse(resource.toString(), 'Unable to stream content')
191+
192+
return badGatewayResponse(resource, 'Unable to stream content')
185193
}
186194
}
187195

packages/verified-fetch/src/plugins/plugin-handle-dag-walk.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ import { BasePlugin } from './plugin-base.js'
66
import type { PluginContext } from './types.js'
77

88
/**
9-
* This plugin should almost always run first because it's going to handle path walking if needed, and will only say it can handle
10-
* the request if path walking is possible (path is not empty, terminalCid is unknown, and the path has not been walked yet).
9+
* This plugin should almost always run first because it's going to handle path
10+
* walking if needed, and will only say it can handle the request if path
11+
* walking is possible (path is not empty, terminalCid is unknown, and the path
12+
* has not been walked yet).
1113
*
12-
* Once this plugin has run, the PluginContext will be updated and then this plugin will return false for canHandle, so it won't run again.
14+
* Once this plugin has run, the PluginContext will be updated and then this
15+
* plugin will return false for canHandle, so it won't run again.
1316
*/
1417
export class DagWalkPlugin extends BasePlugin {
1518
readonly id = 'dag-walk-plugin'
19+
1620
/**
17-
* Return false if the path has already been walked, otherwise return true if the CID is encoded with a codec that supports pathing.
21+
* Return false if the path has already been walked, otherwise return true if
22+
* the CID is encoded with a codec that supports pathing.
1823
*/
1924
canHandle (context: PluginContext): boolean {
20-
this.log('checking if we can handle %c with accept %s', context.cid, context.accept)
2125
const { pathDetails, cid } = context
26+
2227
if (pathDetails != null) {
2328
// path has already been walked
2429
return false

0 commit comments

Comments
 (0)