1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Deno】標準ライブラリのサーバーにContent-Typeを設定する

Posted at

毎回ググってる気がするので、忘備録として記事にまとめておきます。
タイトルを正確に言うと「標準ライブラリのサーバーで、Content-Typeを設定したレスポンスを返す」です。

標準ライブラリのHTTPサーバー

まず標準ライブラリのHTTPサーバーの書き方をおさらいです

HelloWorldサーバー
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.tshttps://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を自動生成するには、以下のようにします。

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);
  }
});
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?