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: add server timing to responses #164

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions packages/verified-fetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,14 @@ Some known header specifications:
- <https://specs.ipfs.tech/http-gateways/trustless-gateway/#response-headers>
- <https://specs.ipfs.tech/http-gateways/subdomain-gateway/#response-headers>

#### Server Timing headers

By default, we do not include [Server Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Server_timing) headers in responses. If you want to include them, you can pass an
`withServerTiming` option to the `createVerifiedFetch` function to include them in all future responses. You can
also pass the `withServerTiming` option to each fetch call to include them only for that specific response.

See PR where this was added, <https://github.com/ipfs/helia-verified-fetch/pull/164>, for more information.

### Possible Scenarios that could cause confusion

#### Attempting to fetch the CID for content that does not make sense
Expand Down
24 changes: 24 additions & 0 deletions packages/verified-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,14 @@
* * https://specs.ipfs.tech/http-gateways/trustless-gateway/#response-headers
* * https://specs.ipfs.tech/http-gateways/subdomain-gateway/#response-headers
*
* #### Server Timing headers
*
* By default, we do not include [Server Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Server_timing) headers in responses. If you want to include them, you can pass an
* `withServerTiming` option to the `createVerifiedFetch` function to include them in all future responses. You can
* also pass the `withServerTiming` option to each fetch call to include them only for that specific response.
*
* See PR where this was added, https://github.com/ipfs/helia-verified-fetch/pull/164, for more information.
*
* ### Possible Scenarios that could cause confusion
*
* #### Attempting to fetch the CID for content that does not make sense
Expand Down Expand Up @@ -750,6 +758,14 @@ export interface CreateVerifiedFetchOptions {
* @default 60000
*/
sessionTTLms?: number

/**
* Whether to include server-timing headers in responses. This option can be overridden on a per-request basis.
*
* @default false
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
*/
withServerTiming?: boolean
}

export type { ContentTypeParser } from './types.js'
Expand Down Expand Up @@ -817,6 +833,14 @@ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<BubbledP
* @default false
*/
allowInsecure?: boolean

/**
* Whether to include server-timing headers in the response for an individual request.
*
* @default false
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
*/
withServerTiming?: boolean
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/verified-fetch/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface FetchHandlerFunctionArg {
* The originally requested resource
*/
resource: string

/**
* Whether to include server-timing headers in the response.
*/
withServerTiming: boolean
}

/**
Expand Down
9 changes: 5 additions & 4 deletions packages/verified-fetch/src/utils/parse-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ export interface ParseResourceComponents {
}

export interface ParseResourceOptions extends ParseUrlStringOptions {

withServerTiming?: boolean
}
/**
* Handles the different use cases for the `resource` argument.
* The resource can represent an IPFS path, IPNS path, or CID.
* If the resource represents an IPNS path, we need to resolve it to a CID.
*/
export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, options?: ParseResourceOptions): Promise<ParsedUrlStringResults> {
export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, { withServerTiming = false, ...options }: ParseResourceOptions = { withServerTiming: false }): Promise<ParsedUrlStringResults> {
if (typeof resource === 'string') {
return parseUrlString({ urlString: resource, ipns, logger }, options)
return parseUrlString({ urlString: resource, ipns, logger, withServerTiming }, options)
}

const cid = CID.asCID(resource)
Expand All @@ -33,7 +33,8 @@ export async function parseResource (resource: Resource, { ipns, logger }: Parse
path: '',
query: {},
ipfsPath: `/ipfs/${cid.toString()}`,
ttl: 29030400 // 1 year for ipfs content
ttl: 29030400, // 1 year for ipfs content
serverTimings: []
} satisfies ParsedUrlStringResults
}

Expand Down
45 changes: 38 additions & 7 deletions packages/verified-fetch/src/utils/parse-url-string.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CID } from 'multiformats/cid'
import { getPeerIdFromString } from './get-peer-id-from-string.js'
import { serverTiming, type ServerTimingResult } from './server-timing.js'
import { TLRU } from './tlru.js'
import type { RequestFormatShorthand } from '../types.js'
import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
Expand All @@ -12,6 +13,7 @@
urlString: string
ipns: IPNS
logger: ComponentLogger
withServerTiming?: boolean
}
export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDNSLinkProgressEvents>, AbortOptions {

Expand All @@ -23,7 +25,7 @@
filename?: string
}

interface ParsedUrlStringResultsBase extends ResolveResult {
export interface ParsedUrlStringResults extends ResolveResult {
protocol: 'ipfs' | 'ipns'
query: ParsedUrlQuery

Expand All @@ -41,9 +43,12 @@
* seconds as a number
*/
ttl?: number
}

export type ParsedUrlStringResults = ParsedUrlStringResultsBase
/**
* serverTiming items
*/
serverTimings: Array<ServerTimingResult<any>>
}

const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
Expand Down Expand Up @@ -145,14 +150,15 @@
* @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
*/
// eslint-disable-next-line complexity
export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
const log = logger.forComponent('helia:verified-fetch:parse-url-string')
const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString)

let cid: CID | undefined
let resolvedPath: string | undefined
const errors: Error[] = []
let resolveResult: IPNSResolveResult | DNSLinkResolveResult | undefined
const serverTimings: Array<ServerTimingResult<any>> = []

if (protocol === 'ipfs') {
try {
Expand Down Expand Up @@ -182,7 +188,19 @@
if (peerId.publicKey == null) {
throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
}
resolveResult = await ipns.resolve(peerId.publicKey, options)

if (withServerTiming) {
const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, ipns.resolve.bind(null, peerId.publicKey, options))
serverTimings.push(resolveResultWithServerTiming)

// eslint-disable-next-line max-depth
if (resolveResultWithServerTiming.error != null) {
throw resolveResultWithServerTiming.error
}
resolveResult = resolveResultWithServerTiming.result ?? undefined

Check warning on line 200 in packages/verified-fetch/src/utils/parse-url-string.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/parse-url-string.ts#L193-L200

Added lines #L193 - L200 were not covered by tests
} else {
resolveResult = await ipns.resolve(peerId.publicKey, options)
}
cid = resolveResult?.cid
resolvedPath = resolveResult?.path
log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
Expand All @@ -207,7 +225,19 @@
log.trace('Attempting to resolve DNSLink for %s', decodedDnsLinkLabel)

try {
resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
// eslint-disable-next-line max-depth
if (withServerTiming) {
const resolveResultWithServerTiming = await serverTiming('ipns.resolveDNSLink', `Resolve DNSLink ${decodedDnsLinkLabel}`, ipns.resolveDNSLink.bind(ipns, decodedDnsLinkLabel, options))
serverTimings.push(resolveResultWithServerTiming)
// eslint-disable-next-line max-depth
if (resolveResultWithServerTiming.error != null) {
throw resolveResultWithServerTiming.error
}
resolveResult = resolveResultWithServerTiming.result ?? undefined

Check warning on line 236 in packages/verified-fetch/src/utils/parse-url-string.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/utils/parse-url-string.ts#L230-L236

Added lines #L230 - L236 were not covered by tests
} else {
resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
}

cid = resolveResult?.cid
resolvedPath = resolveResult?.path
log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
Expand Down Expand Up @@ -263,7 +293,8 @@
path: joinPaths(resolvedPath, urlPath ?? ''),
query,
ttl,
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
serverTimings
} satisfies ParsedUrlStringResults
}

Expand Down
37 changes: 37 additions & 0 deletions packages/verified-fetch/src/utils/server-timing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export interface ServerTimingSuccess<T> {
error: null
result: T
header: string
}
export interface ServerTimingError {
result: null
error: Error
header: string
}
export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError

export async function serverTiming<T> (
name: string,
description: string,
fn: () => Promise<T>
): Promise<ServerTimingResult<T>> {
const startTime = performance.now()

try {
const result = await fn() // Execute the function
const endTime = performance.now()

const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds

// Create the Server-Timing header string
const header = `${name};dur=${duration};desc="${description}"`
return { result, header, error: null }
} catch (error: any) {
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(1)

// Still return a timing header even on error
const header = `${name};dur=${duration};desc="${description}"`
return { result: null, error, header } // Pass error with timing info
}
}
Loading
Loading