npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lark-apaas/nestjs-http-forwarder

v0.1.4

Published

FullStack Nestjs server-side HTTP request forwarder (node egress, mihomo-aware)

Readme

@lark-apaas/nestjs-http-forwarder

NestJS 模块:通用内网转发能力。自动注册 controller,把请求的出口从浏览器变成 node。

⚠️ SDK 不感知代理 / 鉴权 / 隧道。实际客户内网访问的代理 / JWT / 隧道路由由部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)处理。

⚠️ SDK 不内置鉴权 guard。消费方在 AppModule 层面给路由加自己的 guard(如全局 NeedLoginGuard)。

自动注册的接口

ALL  /api/__platform__/http-forward?targetUrl=<内网 URL>
  • method:取自请求自身(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)
  • targetUrl:query 参数
  • headers:自动透传(剔除 host / content-length / hop-by-hop)
  • body:自动透传
  • 响应:上游 status / headers / body 直接投射到 HTTP response

路径硬编码 /api/__platform__/http-forward,不可配置(保证所有消费方调用方式一致)。

使用

1. 注册模块

import { Module } from '@nestjs/common';
import { HttpForwarderModule } from '@lark-apaas/nestjs-http-forwarder';

@Module({
  imports: [
    HttpForwarderModule.forRoot({
      // 全部可选
      requestTimeoutMs: 30_000,
      maxResponseBytes: 10 * 1024 * 1024,
      proxyAuth: { commandTimeoutMs: 5_000 }, // 调优代理鉴权刷新
    }),
  ],
})
export class AppModule {}

2. 前端调用(搭配 axiosForBackend)

import { axiosForBackend } from '@lark-apaas/client-toolkit';

// GET 内网接口
const res = await axiosForBackend.get('/api/__platform__/http-forward', {
  params: { targetUrl: 'http://api.corp.com/v1/users' },
});

// POST 内网接口
await axiosForBackend.post('/api/__platform__/http-forward', { qty: 3 }, {
  params: { targetUrl: 'http://api.corp.com/v1/orders' },
});

// 其他 method 同理
await axiosForBackend.delete('/api/__platform__/http-forward', {
  params: { targetUrl: 'http://api.corp.com/v1/orders/42' },
});

前端拿到的就是真实上游响应:response.status、response.headers、response.data 跟直连内网一样。

3. node 侧调用(服务端业务代码)

后端 service 里访问内网接口,不要裸 fetch / axios(Node 内置 fetch 不读 HTTP_PROXY;代理鉴权在首次 forward() 才 lazy 初始化;也不会带审计头)。注入 HttpForwarderService 用 fetch():

import { Injectable } from '@nestjs/common';
import { HttpForwarderService } from '@lark-apaas/nestjs-http-forwarder';

@Injectable()
export class CorpApiService {
  constructor(private readonly forwarder: HttpForwarderService) {}

  async listOrders() {
    const res = await this.forwarder.fetch('http://api.corp.com/v1/orders', {
      headers: { 'x-api-key': process.env.CORP_API_KEY! }, // 内网凭证只在 node 侧
    });
    if (!res.ok) {
      // 失败也要读完 body:不消费 body 时 undici 连接要等到 requestTimeoutMs(默认 30s)才释放
      throw new Error(`corp api ${res.status}: ${await res.text()}`);
    }
    return res.json();
  }

  async createOrder(payload: { qty: number }) {
    const res = await this.forwarder.fetch('http://api.corp.com/v1/orders', {
      method: 'POST',
      body: payload, // object → JSON.stringify + 自动补 content-type: application/json
    });
    return res.json();
  }
}

fetch() 返回 Web Response(.json() / .text() / .arrayBuffer() / headers.getSetCookie()),上游 4xx/5xx 返回 ok=false 不抛错;forwarder 自身错误(非法 URL / 上游不可达 / 超时 / 非法 header / 不支持的 body)仍抛 HttpException。body 支持 string / JSON-able object / null;Buffer / FormData / URLSearchParams / Blob / 流会被拒绝为 400。

务必消费或取消 body:不读 body 时 undici 连接会一直占用到 requestTimeoutMs(默认 30s)才被超时释放;不需要内容时 await res.body?.cancel()。

适合走 node 侧而不是前端直调的场景:内网接口需要凭证、targetUrl 不应由用户控制、定时任务 / trigger / 服务端聚合。

4. 自定义编排(高级用法)

如果默认 controller 行为不够灵活,可以直接注入 service 写自己的 controller。需要 .json() 等便利方法时用 toWebResponse() 把 ForwardResponseDto 转成 Web Response:

import { Body, Controller, Post } from '@nestjs/common';
import {
  HttpForwarderService,
  toWebResponse,
  type ForwardRequestDto,
} from '@lark-apaas/nestjs-http-forwarder';

@Controller('my/custom')
export class MyController {
  constructor(private readonly forwarder: HttpForwarderService) {}

  @Post()
  async forward(@Body() body: ForwardRequestDto) {
    const out = await this.forwarder.forward(body); // { status, headers, body: Readable }
    const res = toWebResponse(out);                 // → Web Response
    return res.json();
  }
}

ForwardRequestDto / ForwardResponseDto

import type { Readable } from 'node:stream';
import type { IncomingHttpHeaders } from 'undici/types/header';

interface ForwardRequestDto {
  method: string;             // 由 undici 自身校验
  targetUrl: string;          // 必须 http:// 或 https://
  headers?: Record<string, string>;
  body?: string | null;
}

interface ForwardResponseDto {
  status: number;
  headers: IncomingHttpHeaders; // Record<string, string | string[] | undefined>
                                // 多值字段(set-cookie / link / via)保留数组
  body: Readable;               // 未消费的可读流;调用方 pipe / for-await 消费
}

0.2.0 BREAKING:headers 形态从 Record<string, string> 改为 IncomingHttpHeaders,body 从 string 改为 Readable。详情见 docs/plans/2026-05-09-http-forwarder.md Phase 3。

审计上下文 header x-miaoda-ctx

所有走转发通道的出站请求(默认 controller、forward()、fetch())都会带上 x-miaoda-ctx,供客户内网下游审计"是哪个 app / 租户 / 用户 / 是否系统账号"发起的。

  • 编码:标准 base64(RFC 4648 §4,+/ 字母表,带 = padding)的 UTF-8 JSON——不是 base64url

  • 内容(四个 key 永远存在,缺省补 "" / false):

    { "app_id": "app_xxx", "tenant_id": "1234", "user_id": "ou_abc", "is_system_account": false }

    | key | 来源(RequestContextService.getContext()) | 说明 | |---|---|---| | app_id | appId | | | tenant_id | tenantId | 统一转 string | | user_id | userId | 匿名访问为 "" | | is_system_account | isSystemAccount | trigger / 定时任务场景为 true |

  • 不可篡改:调用方(前端或 node 侧)自带的 x-miaoda-ctx 会被 SDK 无条件丢弃并重新生成,与 host 的处理方式一致

  • 信任模型:内容是 app 自述,SDK 不签名——沙箱内业务代码与 SDK 同进程,任何沙箱内密钥都挡不住同 app 内伪造;跨 app 伪造由代理层(per-app JWT → Edge Proxy)鉴权兜底。下游若需要权威 app_id,以代理层鉴权结果为准,不要拿此 header 做授权决策

  • 请求作用域外调用(没有 RequestContext)仍会发 header,四个字段全缺省

TypeScript 侧可引用 MIAODA_CTX_HEADER 常量与 MiaodaCtxPayload 类型。

配置参考

| 字段 | 必填 | 默认 | 说明 | |------|------|------|------| | requestTimeoutMs | — | 30000 | 单次请求超时(AbortController) | | maxResponseBytes | — | 10485760 (10 MB) | 上游响应体最大字节数;超阈值返回 502 RESPONSE_TOO_LARGE | | proxyAuth | — | undefined | v0.3.0 起,出网代理鉴权机制始终启用(按 appId 拉 jwtToken + updateProxyCmd,exec 后定时刷新)。SDK 不感知具体代理实现。本字段仅用于覆盖调优默认值,不传则全用默认。详见 docs/api/nestjs-http-forwarder.md |

service 做的 7 件核心事

| # | 能力 | 说明 | |---|------|------| | 1 | 协议白名单 | targetUrl 仅允许 http:// / https://(防 SSRF) | | 2 | host / content-length 接管 | host 重写为 targetUrl.host;content-length 让 undici 自算 | | 3 | 超时控制 | AbortController + setTimeout 默认 30s;undici 自带 Headers/Body/Connect Timeout 也兜底 | | 4 | 响应体大小上限 | 第一层 content-length 预检超阈值直接 502;第二层 Transform 流中累计字节,超阈值 destroy 流 | | 5 | 错误码映射 | undici 抛错按 signal.aborted / 错误类(HeadersTimeout / InvalidArgument)/ err.code 等结构化信号映射 | | 6 | 出网代理 | EnvHttpProxyAgent 读 HTTP_PROXY / HTTPS_PROXY / NO_PROXY env 注入 dispatcher | | 7 | 审计上下文注入 | 从 RequestContext 生成 x-miaoda-ctx(base64 JSON),调用方自带值无条件覆盖 |

method 白名单 / body 类型校验 / payload 非空校验等都没做——交给 undici 自身(undici 对配置错抛 InvalidArgumentError,由 SDK 映射为 400 INVALID_REQUEST)。

错误码

| HTTP | code | 含义 | |------|------|------| | 400 | INVALID_REQUEST | targetUrl 缺失/非法 URL / undici 抛 InvalidArgumentError(method/headers 配置错) | | 400 | INVALID_TARGET_PROTOCOL | targetUrl 协议非 http/https | | 502 | UPSTREAM_UNREACHABLE | 上游不可达 / 网络错(err.code ∈ ENOTFOUND / ECONNREFUSED / ETIMEDOUT / UND_ERR_*) | | 502 | RESPONSE_TOO_LARGE | 上游响应体超 maxResponseBytes。预检命中时整次请求 502;流中累计兜底时 status 已发出,连接被切断(client 看到截断) | | 504 | UPSTREAM_TIMEOUT | 单次请求超时(controller.signal.aborted 或 undici HeadersTimeoutError / BodyTimeoutError / ConnectTimeoutError) |

上游 4xx / 5xx 不抛错,原样投射到 HTTP response。

当前限制

  • 请求体仅支持 string / null(fetch() 额外支持 JSON-able object);binary / FormData / 流上传待后续版本
  • 响应体流式 pipe 到下游 response,不做 buffer,不支持改写 / 重试
  • 默认 controller 路径固定 /api/__platform__/http-forward,路径自定义请走自定义 controller
  • proxyAuth 始终启用,每个进程严格只服务一个 appId(沙箱-per-app);不同 appId 的请求会被拒绝。消费方必须提供 @lark-apaas/nestjs-common(RequestContextService + PLATFORM_HTTP_CLIENT)