Nest.jsのSSEでMINEタイプが違うエラー
初めに
Nest.js の SSE(Server Sent Events)を使っている時にクライアントサイドで以下のエラーが発生。
EventSource's response has a MIME type ("text/plain") that is not "text/event-stream". Aborting the connection.
どうやらクライアントのEventSource
で受け取ったデータの形式が正しくないとのこと。
SSE 周りは情報が少ないのでググっても解決しなく、自力で解決したので久々に記事を書いていきます。
フレームワーク | バージョン |
---|---|
Next.js(App Router) | 13.4.2 |
Nest.js | 9.0.0 |
問題の再現
バックエンド(Nest.js)のプロジェクト作成
$ npm i -g @nestjs/cli
$ nest new api
? Which package manager would you ❤️ to use? … yarn
$ cd api
$ yarn
main.ts
でcors
の設定とポートを 8000 番に変更します。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.enableCors({
+ origin: 'http://localhost:3000',
+ });
- await app.listen(3000);
+ await app.listen(8000);
}
bootstrap();
ローカルサーバーを立ち上げます
yarn start:dev
フロント(Next.js)のプロジェクト作成
今回は Next.js13.4 から安定版となった App Router を使います。
$ yarn create next-app
? What is your project named? … front
? Would you like to use TypeScript with this project? … Yes
? Would you like to use ESLint with this project? … Yes
? Would you like to use Tailwind CSS with this project? … Yes
? Would you like to use `src/` directory with this project? … Yes
? Use App Router (recommended)? … Yes
? Would you like to customize the default import alias? … No
$ cd front
$ yarn dev
src/app/page.tsx
で不要な記述を削除しておきます。
export default function Home() {
return (
<></>
)
}
SSE を使ってみる
App.Controller
に SSE のエンドポイントを追加します。
内容は公式のものをお借りします。
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
+ @Sse('sse')
+ sse(): Observable<MessageEvent> {
+ return interval(1000).pipe(
+ map((_) => ({ data: { hello: 'world' } } as MessageEvent)),
+ );
+ }
@Get()
getHello() {
return this.appService.getHello();
}
}
次にフロントで受信してみます。
SSE のデータ受信にはEventSource
を使います。EventSource
はクライアント側のコードでしか使えません。
Next.js の Server Component で使うとエラーを吐くので'use client'
で page.tsx を Client Component にします。
+'use client'
+import { useEffect } from "react"
export default function Home() {
+ useEffect(() => {
+ const eventSource = new EventSource(`http://localhost:8000/sse`)
+ eventSource.onmessage = ({ data }: MessageEvent) => {
+ console.log(data)
+ };
+ return () => {
+ eventSource.close();
+ };
+ },[])
return (
<></>
)
}
メッセージを受信してログに出すだけの処理を追加しました
クライアントのコンソールを見てみると一定間隔でデータが受信できているようです。
この時点ではきちんと接続ができていることが確認できます。
問題のコード
ここから開発が進んでいき、controller
の中にエンドポイントが増えていくとします。
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
+ @Post()
+ create() {}
+ @Get()
+ findAll() {}
+ @Get(':id')
+ findOne() {}
+ @Patch(':id')
+ update() {}
+ @Delete(':id')
+ remove() {}
@Sse('sse')
sse(): Observable<MessageEvent> {
return interval(1000).pipe(
map((_) => ({ data: { hello: 'world' } } as MessageEvent)),
);
}
@Get()
getHello() {
return this.appService.getHello();
}
}
この状態でクライアントの localhost:3000 のコンソールを見てみます。
@Sse
のメソッドは触っていないはずですが、送信されるデータの形式が変わってしまったようです。
原因調査のため公式をgoogle翻訳で和訳してみると
これを配置すると、EventSourceクライアント側アプリケーションでクラスのインスタンスを作成し、/sseルート (上記のデコレータに渡したエンドポイントと一致する@Sse()) をコンストラクター引数として渡すことができます。
EventSourceインスタンスは HTTP サーバーへの永続的な接続を開き、イベントをtext/event-stream形式で送信します。接続は、EventSource.close() を呼び出して閉じるまで開いたままになります。
とある。ここで言っていることはこんな感じ(微妙に違ってたらごめんなさい)
- クライアントで
EventSource
インスタンスを生成する - クライアントでインスタンスが作られると
@Sse()
メソッドからの受信を開始する -
@Sse()
メソッドはイベントをtext/event-stream
形式で送信する
つまりこのエンドポイントから送られるデータ形式はtext/event-stream
以外ありえないはず。
であれば何故クライアントはtext/plain
形式でデータを受け取ってしまうのだろうか。
原因
原因は@Patch()
メソッドが@Sse()
メソッドより先に記述されている事が原因でした。
@Patch()
メソッドはクライアントのEventSource
から送られてくる接続リクエストを検知してしまう。そして@Patch()
がtext/plain
形式を返すため、データ形式が正しくない事象が発生していた。
全てのメソッドを試してみたが、どうやら@Patch()
だけが反応している。
解決策
解決方法は@Patch()
メソッドは@Sse()
メソッドより下に記述する。
が、実際のところ@Patch
だけが下部にあるのは気持ち悪いので@Sse()
メソッドは上に置くようにする、が解決策。
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
+ @Sse('sse')
+ sse(): Observable<MessageEvent> {
+ return interval(1000).pipe(
+ map((_) => ({ data: { hello: 'world' } } as MessageEvent)),
+ );
+ }
@Post()
create() {}
@Get()
findAll() {}
@Get(':id')
findOne() {}
@Patch(':id')
update() {}
@Delete(':id')
remove() {}
- @Sse('sse')
- sse(): Observable<MessageEvent> {
- return interval(1000).pipe(
- map((_) => ({ data: { hello: 'world' } } as MessageEvent)),
- );
- }
@Get()
getHello() {
return this.appService.getHello();
}
}
順番を変えたら、正しくデータが受け取れるようになりました。
まとめ
@Sse()
メソッドはtext/event-stream
以外の形式は送信しないはずなので Nest.js と SSE を使っててこのエラーを見たら、順番が違うか接続先間違えてるのどちらかと捉えていいと思う。
EventSource's response has a MIME type ("text/plain") that is not "text/event-stream". Aborting the connection.
こんなニッチな記事が誰かの役に立てば幸いです。
Discussion