TL;DR
サーバでpako.deflateして送信したデータが、クライアントでpako.inflateしようとすると「incorrect header check」エラー、または「undefined」となる問題の対策
■サーバ側
サーバでpako.deflateは "raw:true" しろ
pako.deflate(JSON.stringify(data), { encoding: "utf-8", raw: true });
■クライアント側
base64をatobで変換した文字列をカンマ区切りでUint8Arrayを作成してpako.inflateに渡せ
inflateオプションは{ to: "string", raw: true }としろ
背景
サーバでNode.jsを使いSSE方式で所定のクライアント群にデータを送信する
送信するデータはJSONでpako.deflateで圧縮し、BASE64でクライアントに送信する
Server JSON → pako.deflate → BASE64
↓
Client BASE64 → pako.inflate → JSON.Parse
クライアントではBASE64をエンコードし、Pako.inflateで展開し元のJSON文字列を取得したい。
しかしpako.inflate展開時にエラーでハマった
対策済みコード例
- サーバ側「Worker」が動作しデータを取得
取得した値をdeflateで圧縮
1秒周期でクライアントに送信
const pako = require("pako");
const express = require('express');
const app = express();
const { Worker } = require("worker_threads");
// ワーカースレッドを作成 - データ取得処理
const worker = new Worker("./worker.js");
// 内部バッファの初期化
let internalBuffer = "";
// 接続しているクライアントのリスト
let clients = [];
// ワーカースレッドからのデータを受信
worker.on('message', (data) => {
// 受信したデータを変数に格納
internalBuffer = pako.deflate(JSON.stringify(data), { encoding: "utf-8", raw: true });
});
// 初回のリクエストを受信するエンドポイント
app.get('/api/request', (req, res) => {
res.header("Access-Control-Allow-Origin", "http://localhost:3000");
// SSEの処理
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
// クライアントをリストに追加
clients.push(res);
req.on("close", () => {
// 接続が閉じられた場合、クライアントをリストから削除
clients = clients.filter((client) => client !== res);
});
});
// 毎秒すべてのクライアントにデータをプッシュ
setInterval(() => {
clients.forEach(client => {
client.write(`data:application/octet-stream;base64,${btoa(internalBuffer)}\n\n`);
});
}, 1000);
// サーバを起動
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
- クライアント側 React
取得した値をpako.inflateで展開、JSONを取得
import React, { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import pako from "pako";
const MainComponent: React.FC = () => {
const groupServerAddress = "http://localhost:4000/api/request"
useEffect(() => {
const eventSource = new EventSource(groupServerAddress);
// SSEからデータを受け取る処理
eventSource.onmessage = async (event: MessageEvent) => {
let eventData = event.data;
// データが圧縮されているかどうかをチェック
if (eventData.trim().startsWith("application/octet-stream;base64,")) {
// Base64エンコードされたデータを取得
eventData = eventData.substring("application/octet-stream;base64,".length);
try {
// Base64デコード
const decodedData = atob(eventData.trim());
const sourceArray = new Uint8Array(decodedData.split(",").map(Number));
//inflateで展開 - ここでエラーが出ていた
const decompressedData = pako.inflate(sourceArray, { to: "string", raw: true });
console.log("受信したデータ:", JSON.parse(decompressedData));
}
catch (error) {
console.log(error);
}
}
};
return () => {
eventSource.close();
};
}
}, [groupServerAddress]);
return null;
};
function App() {
return (
<div className="App">
<MainComponent />
</div>
);
}
export default App;
解説
・クライアント React側にてサーバからmessageを受信後
const decodedData = atob(eventData.trim());
でBASE64を変換し、そこからUint8Arrayを作っているが、これは文字列に対する操作になっており
たとえばatob後のデータが以下のケース場合
例)"237,146,77,78,195,78…"
eventData[0] = "2"
eventData[1] = "3"
eventData[2] = "7"
eventData[3] = ","
…
を元にしたByteArrayが生成される。
これをinflateすると「Incorrect header check」となる
//NGコード例
const decodedData = atob(eventData.trim());
const byteNumbers = new Array(decodedData.length);
for (let i = 0; i < decodedData.length; i++) {
byteNumbers[i] = decodedData.charCodeAt(i);
}
// バイト値の配列をUint8Arrayに変換
const byteArray = new Uint8Array(byteNumbers);
//Incorrect header checkとなる
const decompressedData = pako.inflate(sourceArray, { to: "string", raw: true });
元データの通り
eventData[0] = "237"
eventData[1] = "146"
eventData[2] = "77"
…
としたUint8Arrayをinflateすれば解決する。
inflateのエラー
・エラー「Incorrect header check」は、データの圧縮形式に問題があることを示す
・エラーは出ないが、結果が「undefined」は渡されるデータが誤っている