毎回ググってる気がするので、忘備録として記事にまとめておきます。
タイトルを正確に言うと「標準ライブラリのサーバーで、Content-Typeを設定したレスポンスを返す」です。
標準ライブラリのHTTPサーバー
まず標準ライブラリのHTTPサーバーの書き方をおさらいです
import { serve } from "https://deno.land/std@0.115.1/http/mod.ts";
console.log("http://localhost:8000/");
serve((request: Request) => new Response("Hello World")); // デフォルトはポート8000を使用
単純なファイルサーバーとして使うには、次のように書きます。
import {
serve,
Status,
STATUS_TEXT,
} from "https://deno.land/std@0.115.1/http/mod.ts";
console.log("http://localhost:8000/");
serve(async (request) => {
try {
// 読み込むファイルのurlを作成
const { pathname } = new URL(request.url);
const url = new URL(`.${pathname}`, import.meta.url);
// ファイルをfetchしてResponseを返す
return await fetch(url);
} catch (error) {
// NotFoundの時はTypeErrorが発生
console.log(error);
const status = Status.NotFound;
return new Response(`${status} ${STATUS_TEXT.get(status)}`, { status });
}
});
ところで、上記の例ではレスポンスヘッダにContent-Typeが設定されていません。
Content-Typeを付けたい場合は、ファイルの拡張子から自動でContent-Typeを設定する方法があります。
Content-Typeの設定方法
https://deno.land/std@0.115.1/path/mod.ts と https://deno.land/x/media_types@v2.11.0/mod.ts を使用します。(バージョンは現時点での最新版。適宜アップデートしてください。)
前者は標準ライブラリのpathモジュールです。ファイル名やファイルパスから拡張子を見つけるために使います。
import { extname } from "https://deno.land/std@0.115.1/path/mod.ts";
console.log(extname("foobar.txt")) //=> ".txt"
console.log(extname(".txt")) //=> ""
後者のmedia_typesモジュールは、拡張子からContent-Typeを見つけるために使います。
人気ミドルウェアフレームワークのoak内で維持管理されており、充分安定しています。
import { contentType } from "https://deno.land/x/media_types@v2.11.0/mod.ts";
console.log(contentType(".txt")) //=> "text/plain; charset=utf-8"
console.log(contentType(".hogehoge")) //=> undefined
console.log(contentType("")) //=> undefined
これらのモジュールを使って、ファイルサーバーでファイル名からContent-Typeを自動生成するには、以下のようにします。
import {
serve,
Status,
STATUS_TEXT,
} from "https://deno.land/std@0.115.1/http/mod.ts";
import { contentType } from "https://deno.land/x/media_types@v2.11.0/mod.ts";
import { extname } from "https://deno.land/std@0.115.1/path/mod.ts";
console.log("http://localhost:8000/");
serve(async (request) => {
try {
// 読み込むファイルのurlを作成
const { pathname } = new URL(request.url);
const url = new URL(`.${pathname}`, import.meta.url);
const type = contentType(extname(pathname)) || "text/plain; charset=utf-8";
// ファイルをfetchしてResponseを返す
const response = await fetch(url);
return new Response(response.body, { headers: { "Content-Type": type } });
} catch (error) {
// NotFoundの時はTypeErrorが発生
console.log(error);
const status = Status.NotFound;
return new Response(`${status} ${STATUS_TEXT.get(status)}`, { status });
}
});
ここで注意点として、fetchの返り値のResonseオブジェクトに直接Content-Typeを設定することはできません。つまり、以下のコードは実行時エラーになります。
response.headers.set("Content-Type", type);
これは、"fetchのイミュータブルガード"というのがあるらしく、fetchの返り値のResponseオブジェクトは書き換えできない事になっているためです。
書き換えできないため、新しいResponseオブジェクトを作成してContent-Typeを設定する必要があります。
const response = await fetch(url);
// response.bodyはReadableStream
return new Response(response.body, { headers: { "Content-Type": type } });
おまけ
上に書いたファイルサーバーに
- 末尾スラッシュをindex.htmlにルーティングする機能
- GETリクエスト以外は弾く
- ちゃんとしたエラーハンドリング
を付加したものを置いておきます。
import {
serve,
Status,
STATUS_TEXT,
} from "https://deno.land/std@0.115.1/http/mod.ts";
import { contentType } from "https://deno.land/x/media_types@v2.11.0/mod.ts";
import { extname } from "https://deno.land/std@0.115.1/path/mod.ts";
/** 拡張子からcontentTypeを生成する */
function contentTypeFromExt(ext: string): string {
return contentType(ext) || "text/plain; charset=utf-8";
}
/** ステータスコードからResponseオブジェクトを生成する */
function responseFromStatus(status: Status) {
return new Response(`${status} ${STATUS_TEXT.get(status)}`, { status });
}
console.log("http://localhost:8000/");
serve(async (request) => {
try {
// GETメソッド以外は弾く
if (request.method !== "GET") {
return responseFromStatus(Status.MethodNotAllowed);
}
// 読み込むファイルのurlを作成
let { pathname } = new URL(request.url);
if (pathname.at(-1) === "/") {
pathname += "index.html";
}
const url = new URL(`.${pathname}`, import.meta.url);
let response;
try {
response = await fetch(url);
} catch {
// NotFoundの時はTypeErrorが発生
return responseFromStatus(Status.NotFound);
}
// response headerを設定するため、新たにResponseオブジェクトを生成する
return new Response(response.body, {
headers: { "Content-Type": contentTypeFromExt(extname(pathname)) },
});
} catch (e) {
console.log(e);
// エラーが発生した場合は500 Internal Server Error
return responseFromStatus(Status.InternalServerError);
}
});