Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support redirects for UnixFS directories #5

Merged
merged 2 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/verified-fetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,79 @@ console.info(res.headers.get('accept')) // application/octet-stream
const buf = await res.arrayBuffer() // raw bytes, not processed as JSON
```

## Redirects

If a requested URL contains a path component, that path component resolves to
a UnixFS directory, but the URL does not have a trailing slash, one will be
added to form a canonical URL for that resource, otherwise the request will
be resolved as normal.

```typescript
import { verifiedFetch } from '@helia/verified-fetch'

const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir')

console.info(res.url) // ipfs://bafyfoo/path/to/dir/
```

It's possible to prevent this behaviour and/or handle a redirect manually
through use of the [redirect](https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect)
option.

## Example - Redirect: follow

This is the default value and is what happens if no value is specified.

```typescript
import { verifiedFetch } from '@helia/verified-fetch'

const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
redirect: 'follow'
})

console.info(res.status) // 200
console.info(res.url) // ipfs://bafyfoo/path/to/dir/
console.info(res.redirected) // true
```

## Example - Redirect: error

This causes a `TypeError` to be thrown if a URL would cause a redirect.

```typescript

import { verifiedFetch } from '@helia/verified-fetch'

const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
redirect: 'error'
})
// throw TypeError('Failed to fetch')
```

## Example - Redirect: manual

Manual redirects allow the user to process the redirect. A [301](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301)
is returned, and the location to redirect to is available as the "location"
response header.

This differs slightly from HTTP fetch which returns an opaque response as the
browser itself is expected to process the redirect and hide all details from
the user.

```typescript

import { verifiedFetch } from '@helia/verified-fetch'

const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
redirect: 'manual'
})

console.info(res.status) // 301
console.info(res.url) // ipfs://bafyfoo/path/to/dir
console.info(res.redirected) // false
console.info(res.headers.get('location')) // ipfs://bafyfoo/path/to/dir/
```

## Comparison to fetch

This module attempts to act as similarly to the `fetch()` API as possible.
Expand Down
73 changes: 73 additions & 0 deletions packages/verified-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,79 @@
* const buf = await res.arrayBuffer() // raw bytes, not processed as JSON
* ```
*
* ## Redirects
*
* If a requested URL contains a path component, that path component resolves to
* a UnixFS directory, but the URL does not have a trailing slash, one will be
* added to form a canonical URL for that resource, otherwise the request will
* be resolved as normal.
*
* ```typescript
* import { verifiedFetch } from '@helia/verified-fetch'
*
* const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir')
*
* console.info(res.url) // ipfs://bafyfoo/path/to/dir/
* ```
*
* It's possible to prevent this behaviour and/or handle a redirect manually
* through use of the [redirect](https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect)
* option.
*
* @example Redirect: follow
*
* This is the default value and is what happens if no value is specified.
*
* ```typescript
* import { verifiedFetch } from '@helia/verified-fetch'
*
* const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
* redirect: 'follow'
* })
*
* console.info(res.status) // 200
* console.info(res.url) // ipfs://bafyfoo/path/to/dir/
* console.info(res.redirected) // true
* ```
*
* @example Redirect: error
*
* This causes a `TypeError` to be thrown if a URL would cause a redirect.
*
* ```typescript
*
* import { verifiedFetch } from '@helia/verified-fetch'
*
* const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
* redirect: 'error'
* })
* // throw TypeError('Failed to fetch')
* ```
*
* @example Redirect: manual
*
* Manual redirects allow the user to process the redirect. A [301](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301)
* is returned, and the location to redirect to is available as the "location"
* response header.
*
* This differs slightly from HTTP fetch which returns an opaque response as the
* browser itself is expected to process the redirect and hide all details from
* the user.
*
* ```typescript
*
* import { verifiedFetch } from '@helia/verified-fetch'
*
* const res = await verifiedFetch('ipfs://bafyfoo/path/to/dir', {
* redirect: 'manual'
* })
*
* console.info(res.status) // 301
* console.info(res.url) // ipfs://bafyfoo/path/to/dir
* console.info(res.redirected) // false
* console.info(res.headers.get('location')) // ipfs://bafyfoo/path/to/dir/
* ```
*
* ## Comparison to fetch
*
* This module attempts to act as similarly to the `fetch()` API as possible.
Expand Down
83 changes: 76 additions & 7 deletions packages/verified-fetch/src/utils/responses.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,98 @@
export function okResponse (body?: BodyInit | null): Response {
return new Response(body, {
function setField (response: Response, name: string, value: string | boolean): void {
Object.defineProperty(response, name, {
enumerable: true,
configurable: false,
set: () => {},
get: () => value
})
}

function setType (response: Response, value: 'basic' | 'cors' | 'error' | 'opaque' | 'opaqueredirect'): void {
setField(response, 'type', value)
}

function setUrl (response: Response, value: string): void {
setField(response, 'url', value)
}

function setRedirected (response: Response): void {
setField(response, 'redirected', true)
}

export interface ResponseOptions extends ResponseInit {
redirected?: boolean
}

export function okResponse (url: string, body?: BodyInit | null, init?: ResponseOptions): Response {
const response = new Response(body, {
...(init ?? {}),
status: 200,
statusText: 'OK'
})

if (init?.redirected === true) {
setRedirected(response)
}

setType(response, 'basic')
setUrl(response, url)

return response
}

export function notSupportedResponse (body?: BodyInit | null): Response {
export function notSupportedResponse (url: string, body?: BodyInit | null, init?: ResponseInit): Response {
const response = new Response(body, {
...(init ?? {}),
status: 501,
statusText: 'Not Implemented'
})
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header

setType(response, 'basic')
setUrl(response, url)

return response
}

export function notAcceptableResponse (body?: BodyInit | null): Response {
return new Response(body, {
export function notAcceptableResponse (url: string, body?: BodyInit | null, init?: ResponseInit): Response {
const response = new Response(body, {
...(init ?? {}),
status: 406,
statusText: 'Not Acceptable'
})

setType(response, 'basic')
setUrl(response, url)

return response
}

export function badRequestResponse (body?: BodyInit | null): Response {
return new Response(body, {
export function badRequestResponse (url: string, body?: BodyInit | null, init?: ResponseInit): Response {
const response = new Response(body, {
...(init ?? {}),
status: 400,
statusText: 'Bad Request'
})

setType(response, 'basic')
setUrl(response, url)

return response
}

export function movedPermanentlyResponse (url: string, location: string, init?: ResponseInit): Response {
const response = new Response(null, {
...(init ?? {}),
status: 301,
statusText: 'Moved Permanently',
headers: {
...(init?.headers ?? {}),
location
}
})

setType(response, 'basic')
setUrl(response, url)

return response
}
Loading
Loading