diff --git a/.changeset/chatty-ears-wash.md b/.changeset/chatty-ears-wash.md
new file mode 100644
index 00000000..eb132714
--- /dev/null
+++ b/.changeset/chatty-ears-wash.md
@@ -0,0 +1,5 @@
+---
+"frog": patch
+---
+
+Widened handler return types to allow [`Response` objects](https://developer.mozilla.org/en-US/docs/Web/API/Response).
diff --git a/playground/src/transaction.tsx b/playground/src/transaction.tsx
index 97f515bd..f7518760 100644
--- a/playground/src/transaction.tsx
+++ b/playground/src/transaction.tsx
@@ -2,8 +2,8 @@ import { Button, Frog } from 'frog'
export const app = new Frog()
-app.frame('/', () => {
- return {
+app.frame('/', (c) => {
+ return c.res({
image: (
Example
@@ -14,7 +14,7 @@ app.frame('/', () => {
Send Transaction,
Mint,
],
- }
+ })
})
// Raw transaction
diff --git a/site/pages/reference/frog-transaction-response.mdx b/site/pages/reference/frog-transaction-response.mdx
index 25b0a412..85267fa9 100644
--- a/site/pages/reference/frog-transaction-response.mdx
+++ b/site/pages/reference/frog-transaction-response.mdx
@@ -43,6 +43,21 @@ app.transaction('/raw-send', (c) => {
})
```
+:::tip
+Frog also supports [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects as a return value of the `.transaction` handler. Useful for returning error responses.
+
+```tsx twoslash
+// @noErrors
+import { Frog, parseEther } from 'frog'
+
+export const app = new Frog()
+
+app.transaction('/send-ether', (c) => {
+ return new Response('dang', { status: 400 }) // [!code focus]
+})
+```
+:::
+
## Send Transaction (`c.send`)
### chainId
diff --git a/src/frog-base.tsx b/src/frog-base.tsx
index 23c16238..a238b9ba 100644
--- a/src/frog-base.tsx
+++ b/src/frog-base.tsx
@@ -15,6 +15,7 @@ import {
type FrameResponse,
} from './types/frame.js'
import type { Hub } from './types/hub.js'
+import type { HandlerResponse } from './types/response.js'
import { type Pretty } from './types/utils.js'
import { fromQuery } from './utils/fromQuery.js'
import { getButtonValues } from './utils/getButtonValues.js'
@@ -261,7 +262,7 @@ export class FrogBase<
path: path,
handler: (
context: Pretty
>,
- ) => FrameResponse | Promise,
+ ) => HandlerResponse,
options: RouteOptions = {},
) {
const { verify = this.verify } = options
@@ -287,6 +288,9 @@ export class FrogBase<
if (context.url !== parsePath(c.req.url)) return c.redirect(context.url)
+ const response = await handler(context)
+ if (response instanceof Response) return response
+
const {
action,
browserLocation = this.browserLocation,
@@ -295,7 +299,7 @@ export class FrogBase<
image,
intents,
title = 'Frog Frame',
- } = await handler(context)
+ } = response.data
const buttonValues = getButtonValues(parseIntents(intents))
if (context.status === 'redirect' && context.buttonIndex) {
@@ -472,11 +476,14 @@ export class FrogBase<
? await this.imageOptions()
: this.imageOptions
+ const response = await handler(context)
+ if (response instanceof Response) return response
+
const {
image,
headers = this.headers,
imageOptions = defaultImageOptions,
- } = await handler(context)
+ } = response.data
if (typeof image === 'string') return c.redirect(image, 302)
return new ImageResponse(image, {
...imageOptions,
diff --git a/src/frog.tsx b/src/frog.tsx
index 481a898f..d8a7dc28 100644
--- a/src/frog.tsx
+++ b/src/frog.tsx
@@ -4,6 +4,7 @@ import { routes as devRoutes } from './dev/routes.js'
import { FrogBase, type RouteOptions } from './frog-base.js'
import { type FrameContext } from './types/context.js'
import { type FrameResponse } from './types/frame.js'
+import type { HandlerResponse } from './types/response.js'
import { type Pretty } from './types/utils.js'
/**
@@ -46,7 +47,7 @@ export class Frog<
path: path,
handler: (
context: Pretty>,
- ) => FrameResponse | Promise,
+ ) => HandlerResponse,
options: RouteOptions = {},
) {
super.frame(path, handler, options)
diff --git a/src/routes/transaction.ts b/src/routes/transaction.ts
index 80d3455b..dac76c40 100644
--- a/src/routes/transaction.ts
+++ b/src/routes/transaction.ts
@@ -2,6 +2,7 @@ import type { Env } from 'hono'
import type { FrogBase, RouteOptions } from '../frog-base.js'
import type { TransactionContext } from '../types/context.js'
+import type { HandlerResponse } from '../types/response.js'
import type { TransactionResponse } from '../types/transaction.js'
import { getTransactionContext } from '../utils/getTransactionContext.js'
import { parsePath } from '../utils/parsePath.js'
@@ -12,7 +13,7 @@ export function transaction(
path: string,
handler: (
context: TransactionContext,
- ) => TransactionResponse | Promise,
+ ) => HandlerResponse,
options: RouteOptions = {},
) {
const { verify = this.verify } = options
@@ -27,7 +28,8 @@ export function transaction(
}),
req: c.req,
})
- const transaction = await handler(transactionContext)
- return c.json(transaction)
+ const response = await handler(transactionContext)
+ if (response instanceof Response) return response
+ return c.json(response.data)
})
}
diff --git a/src/types/frame.ts b/src/types/frame.ts
index da309444..3b470a73 100644
--- a/src/types/frame.ts
+++ b/src/types/frame.ts
@@ -1,4 +1,5 @@
import { type ImageResponseOptions } from 'hono-og'
+import type { TypedResponse } from './response.js'
export type FrameResponse = {
/**
@@ -105,7 +106,9 @@ export type FrameResponse = {
title?: string | undefined
}
-export type FrameResponseFn = (response: FrameResponse) => FrameResponse
+export type FrameResponseFn = (
+ response: FrameResponse,
+) => TypedResponse
export type FrameData = {
buttonIndex?: 1 | 2 | 3 | 4 | undefined
diff --git a/src/types/response.ts b/src/types/response.ts
new file mode 100644
index 00000000..693fc9af
--- /dev/null
+++ b/src/types/response.ts
@@ -0,0 +1,10 @@
+export type TypedResponse = {
+ data: data
+ format: 'frame' | 'transaction'
+}
+
+export type HandlerResponse =
+ | Response
+ | TypedResponse
+ | Promise
+ | Promise>
diff --git a/src/types/transaction.ts b/src/types/transaction.ts
index b4f8a233..7ad8d3e4 100644
--- a/src/types/transaction.ts
+++ b/src/types/transaction.ts
@@ -5,6 +5,7 @@ import type {
GetValue,
Hex,
} from 'viem'
+import type { TypedResponse } from './response.js'
import type { UnionWiden, Widen } from './utils.js'
//////////////////////////////////////////////////////
@@ -38,7 +39,7 @@ export type EthSendTransactionParameters = {
export type TransactionResponseFn = (
parameters: parameters,
-) => TransactionResponse
+) => TypedResponse
//////////////////////////////////////////////////////
// Send Transaction
@@ -85,4 +86,4 @@ export type ContractTransactionResponseFn = <
>,
>(
response: ContractTransactionParameters,
-) => TransactionResponse
+) => TypedResponse
diff --git a/src/utils/getFrameContext.ts b/src/utils/getFrameContext.ts
index 199580e5..feaba2d3 100644
--- a/src/utils/getFrameContext.ts
+++ b/src/utils/getFrameContext.ts
@@ -61,7 +61,7 @@ export function getFrameContext(
previousButtonValues,
previousState: previousState as any,
req,
- res: (data) => data,
+ res: (data) => ({ data, format: 'frame' }),
status,
transactionId: frameData?.transactionId,
url,
diff --git a/src/utils/getTransactionContext.ts b/src/utils/getTransactionContext.ts
index c4e1daa5..d58f0000 100644
--- a/src/utils/getTransactionContext.ts
+++ b/src/utils/getTransactionContext.ts
@@ -81,7 +81,7 @@ export function getTransactionContext(
},
}
if (value) response.params.value = value.toString()
- return response
+ return { data: response, format: 'transaction' }
},
send(parameters) {
return this.res({