跳到主要内容
版本:4.0.0

数据响应

从 v3.17.0 开始,框架添加了 ServerResponseHttpServerResponse 的实现。

通过这个功能,可以定制服务端的响应成功和失败时的通用格式,规范整个返回逻辑。

Http 通用响应

在 koa 场景下,一般都会处理一些逻辑,最后返回一个结果。在此过程中,会出现返回成功和失败的情况。

最为常见实现会在 ctx 增加一些方法,包括数据后返回。

import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
try {
// ...
return this.ctx.ok(/*...*/);
} catch (err) {
return this.ctx.fail(/*...*/);
}
}
}

也有人会在 Web 中间件中处理成功的返回,在错误过滤器中处理失败的返回。

为了解决这类代码难以统一维护的问题,框架提供了一套统一返回的方案。

我们以最为常见的返回 JSON 数据为例。

通过创建 HttpServerResponse 实例后,调用 json() 方法,链式返回数据。

import { Controller, Get, Inject, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/success')
async home() {
return new HttpServerResponse(this.ctx).success().json({
// ...
});
}

@Get('/fail')
async home2() {
return new HttpServerResponse(this.ctx).fail().json({
// ...
});
}
}

默认情况下,HttpServerResponse 在成功和失败的场景上会提供 JSON 的通用包裹结构。

比如在成功的场景下,接收到的数据如下。

{
success: 'true',
data: //...
}

而在失败的场景下,接收到的数据如下。

{
success: 'false',
message: //...
}

注意,json() 方法是数据设置的方法,必须在最后一个调用。

常用的响应格式

HttpServerResponse 需要传递一个当前请求的上下文对象 ctx 才能实例化。

const serverResponse = new HttpServerResponse(this.ctx);

之后以链式的形式进行调用。

// json
serverResponse.json({
a: 1,
});
// text
serverResponse.text('abcde');
// blob
serverResponse.blob(Buffer.from('hello world'));

除了设置数据的方法,还提供了一些其他的快捷方法可以组合使用。

// status
serverResponse.status(200).text('abcde');
// header
serverResponse.header('Content-Type', 'text/html').text('<div>hello</div>');
// headers
serverResponse.headers({
'Content-Type': 'text/plain',
'Content-Length': '100'
}).text('a'.repeat(100));

响应模版

针对不同的设置数据的方法,框架提供了不同模版以供用户自定义。

比如 json() 方法的模版如下。

class ServerResponse {
// ...
static JSON_TPL = (data: Record<any, any>, isSuccess: boolean): unknown => {
if (isSuccess) {
return {
success: 'true',
data,
};
} else {
return {
success: 'false',
message: data || 'fail',
};
}
};
}

我们可以将全局的模版进行覆盖达到自定义的目的。

HttpServerResponse.JSON_TPL = (data, isSuccess) => {
if (isSuccess) {
// ...
} else {
// ...
}
};

也可以通过继承,自定义不同的响应模版,这样可以不影响全局的默认模板。

class CustomServerResponse extends HttpServerResponse {}
CustomServerResponse.JSON_TPL = (data, isSuccess) => {
if (isSuccess) {
// ...
} else {
// ...
}
};

在使用时,创建实例即可。

// ...

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
return new CustomServerResponse(this.ctx).success().json({
// ...
});
}
}

此外,针对 textblob 方法的模版均可以覆盖。

HttpServerResponse.TEXT_TPL = (data, isSuccess) => { /*...*/};
HttpServerResponse.BLOB_TPL = (data, isSuccess) => { /*...*/};

数据流式响应

使用内置的 HttpServerResponse 中的 stream 方法来处理流式数据返回。

import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
const res = new HttpServerResponse(this.ctx).stream();
setTimeout(() => {
for (let i = 0; i < 100; i++) {
await sleep(100);
res.send('abc'.repeat(100));
}

res.end();
}, 1000);
return res;
}
}

通过 STEAM_TPL 可以修改数据的返回结构

HttpServerResponse.STREAM_TPL = (data) => { /*...*/};

注意,这个模版只处理成功的数据。

文件流式响应

从 v3.17.0 开始,可以通过 HttpServerResponse 简单处理文件下载。

传递一个文件路径即可,默认会使用 application/octet-stream 响应头返回。

import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
const filePath = join(__dirname, '../../package.json');
return new HttpServerResponse(this.ctx).file(filePath);
}
}

如需返回不同的类型,可以通过第二个参数指定类型。

import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
const filePath = join(__dirname, '../../package.json');
return new HttpServerResponse(this.ctx).file(filePath, 'application/json');
}
}

通过 FILE_TPL 可以修改返回结构。

HttpServerResponse.FILE_TPL = (data: Readable, isSuccess: boolean) => { /*...*/};

SSE 响应

从 v3.17.0 开始,框架提供了内置的 SSE (Server-Sent Events)支持。

SSE 的数据定义如下,你需要按下面的格式返回。

export interface ServerSendEventMessage {
data?: string | object;
event?: string;
id?: string;
retry?: number;
}

通过 HttpServerResponse 定义一个返回实例。

import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
const res = new HttpServerResponse(this.ctx).sse();
// ...
return res;
}
}

可以通过 sendsendEnd 进行数据传递。

const res = new HttpServerResponse(this.ctx).sse();

res.send({
data: 'abcde'
});

res.sendEnd({
data: 'end'
});

调用 sendEnd 后,请求将被关闭。

也可以通过 sendError 发送错误。

const res = new HttpServerResponse(this.ctx).sse();

res.sendError(new Error('test error'));

转发 AI SDK 的 SSE 响应

在一些 AI 网关场景中,服务端需要负责鉴权、隐藏系统提示词、组装工具参数,然后把 OpenAI、Anthropic 等 SDK 的流式返回原样转发给前端。

sse() 返回的对象提供了 forward() 方法,可以把 SDK 返回的 AsyncIterable 转换为客户端可解析的 SSE 响应。

当前内置支持 openaianthropic 两种 SDK 协议格式。其他 SDK 可以使用通用的 eventsource 协议,或者通过 transform 转换为自定义事件结构。

安装 SDK:

npm i openai @anthropic-ai/sdk

也可以在 package.json 中声明依赖:

{
"dependencies": {
"openai": "^6.35.0",
"@anthropic-ai/sdk": "^0.92.0"
}
}

OpenAI 示例:

import { Controller, Get, Inject, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import OpenAI from 'openai';

const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/openai')
async openai() {
const upstream = await client.chat.completions.create({
model: 'gpt-4o-mini', // 替换为你要使用的模型
messages: [
{
role: 'system',
content: '你是一个有帮助的助手。',
},
{
role: 'user',
content: '请介绍 Midway。',
},
],
stream: true,
});

const res = new HttpServerResponse(this.ctx).sse();
res.forward(upstream, {
protocol: 'openai',
});

return res;
}
}

protocol: 'openai' 会输出 OpenAI 客户端可解析的 SSE 数据帧,并在上游正常结束时发送 data: [DONE]

Anthropic 示例:

import { Controller, Get, Inject, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/anthropic')
async anthropic() {
const upstream = client.messages.stream({
model: 'claude-sonnet-4-5', // 替换为你要使用的模型
max_tokens: 2048,
messages: [
{
role: 'user',
content: '请介绍 Midway。',
},
],
thinking: {
type: 'enabled',
budget_tokens: 1024,
},
});

const res = new HttpServerResponse(this.ctx).sse();
res.forward(upstream, {
protocol: 'anthropic',
});

return res;
}
}

protocol: 'anthropic' 会保留 Anthropic 的事件名,例如 message_startcontent_block_deltamessage_stop 等,前端可以继续按 Anthropic 的事件格式解析。

如果只需要普通浏览器 EventSource 或自定义前端解析器,可以使用默认的 eventsource 协议。

const res = new HttpServerResponse(this.ctx).sse();

res.forward(upstream, {
protocol: 'eventsource',
});

return res;

forward() 也支持对事件做轻量处理。返回 null 表示跳过当前事件。

const res = new HttpServerResponse(this.ctx).sse();

res.forward(upstream, {
protocol: 'anthropic',
transform: chunk => {
if (chunk.type === 'ping') {
return null;
}
return chunk;
},
});

return res;

当上游 SDK 调用传入了 AbortController 时,也可以把它传给 forward()。客户端断开连接后,框架会调用 abort(),用于终止上游请求。

const abortController = new AbortController();
const upstream = await client.chat.completions.create({
model: 'gpt-4o-mini', // 替换为你要使用的模型
messages,
stream: true,
}, {
signal: abortController.signal,
});

const res = new HttpServerResponse(this.ctx).sse();
res.forward(upstream, {
protocol: 'openai',
abortController,
});

return res;
信息

forward() 只负责把 SDK 流式事件转换为对应协议的 SSE 响应,不会解析或改写模型返回的 thinkingreasoning、工具调用等内容。如果前端需要展示这些信息,请在前端按 OpenAI 或 Anthropic 的事件格式自行解析。

通过 SSE_TPL 可以修改返回结构。

import { ServerSendEventMessage } from '@midwayjs/core';

HttpServerResponse.SSE_TPL = (data: ServerSendEventMessage) => { /*...*/};

注意,这个模版只处理成功的数据,不会处理 sendError 的情况,且返回也必须是 ServerSendEventMessage 格式。

基础数据响应

除了 Http 场景之外,框架提供了基础的 ServerResponse 类,用于其他的场景。

ServerResponse 包含 jsontextblob 三种数据返回方法,以及 successfail 这两个设置状态的方法。

行为和 HttpServerResponse 一致。

通过继承、覆盖等行为,可以非常简单的处理响应值。

比如我们对不同的用户做返回区分。

// src/response/api.ts
export class UserServerResponse extends HttpServerResponse {}
UserServerResponse.JSON_TPL = (data, isSuccess) => {
if (isSuccess) {
return {
status: 200,
...data,
};
} else {
return {
status: 500,
message: 'limit exceed'
};
}
};

export class AdminServerResponse extends HttpServerResponse {}
AdminServerResponse.JSON_TPL = (data, isSuccess) => {
if (isSuccess) {
return {
status: 200,
router: data.router,
...data
};
} else {
return {
status: 500,
message: 'interal error',
...data
};
}
};

使用返回。

import { Controller, Get, Inject, sleep, HttpServerResponse } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { UserServerResponse, AdminServerResponse } from '../response/api';

@Controller('/')
export class HomeController {
@Inject()
ctx: Context;

@Get('/')
async home() {
// ...
if (this.ctx.user === 'xxx') {
return new AdminServerResponse(this.ctx).json({
router: '/',
dbInfo: {
// ...
},
userInfo: {
role: 'admin',
},
status: 'ok',
});
}
return new UserServerResponse(this.ctx).json({
status: 'ok',
});
}
}