every Tech Blog

株式会社エブリーのTech Blogです。

Go 言語で multipart/form-data を使用して画像を受け取り外部に送信する

この記事は every Tech Blog Advent Calendar 2024(夏) 25 日目の記事です。

はじめに

こんにちは、24 新卒として 4 月から入社し、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている新谷です。

現在取り組んでいる業務で、画像を受け取って外部に送信する処理を行う API を作成する機会がありました。そこで学んだ、Go 言語における multipart/form-data を使った画像の取り扱い方について紹介します。

背景

開発した機能としては、クライアントがアップロードした画像を API サーバーで受け取り、外部の API に POST するというものです。開発した部分は以下の図で表すと、API サーバーの部分になります。

この処理を実現するためには、画像データの受信かつ送信を行う必要があります。今回は、画像を取り扱う際によく使われるmultipart/form-data形式を使用して画像を受け取り、外部 API に送信するように実装したので、その方法について紹介します。

multipart/form-data とは

multipart/form-dataは、Web ページのフォームからファイルをアップロードする際に使用されるデータ形式です。multipart/form-data形式のリクエストは、以下のような特徴があります。

  • リクエストボディが複数の部品(part)から構成される
  • 各部品はヘッダーとボディから構成される
  • 各部品はboundaryで区切られる
  • ヘッダーには Content-Disposition や Content-Type などが含まれる
  • ボディにはファイルのデータが含まれる

以下は、multipart/form-data形式のリクエストの例です。

POST /post HTTP/1.1
Host: httpbin.org
User-Agent: Go-http-client/1.1
Content-Length: 376
Content-Type: multipart/form-data; boundary=600fcf99bf89273352a59587b26bc07642bd
bfc97ce30f8445ecc0c3873a
Accept-Encoding: gzip

--600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a
Content-Disposition: form-data; name="file"; filename="sample.txt"
Content-Type: application/octet-stream

this is dummy.

--600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a
Content-Disposition: form-data; name="key1"

value1
--600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a--

ここでは、boundaryで区切られた 2 つの部品が含まれています。1 つ目の部品はファイルのデータを含み、2 つ目の部品はフォームデータを含んでいます。

実装

API Server での画像の受け取り方

本 API Server は、Go の echo フレームワークを使用しています。echo のc.FormFileを使用して画像を受け取る実装を以下に示します。

   file, err := c.FormFile("image_file")
    if err != nil {
        return types.ErrInvalidParameters
    }

    src, err := file.Open()
    if err != nil {
        return types.ErrInvalidParameters
    }
    defer src.Close()

それぞれの処理について解説します。

c.FormFile("image_file")は、リクエストから"image_file"という名前のフィールドを取得します。先ほどのmultipart/form-data形式のリクエストの例では、name="file"としていた部分に対応します。

image_fileフィールドから取得したfileにはmultipart.FileHeader型のファイル情報が格納され、ファイル名やファイルサイズなどの情報を取得できます。

次に、file.Open()でファイルを開き、srcにファイルの中身を格納します。file.Open()は、ファイルを開いてmultipart.File型のファイルオブジェクトを返します。multipart.Fileは以下のインターフェースを実装しています。

type File interface {
    io.Reader
    io.ReaderAt
    io.Seeker
    io.Closer
}

io.Readerio.ReaderAtio.Seekerio.Closerのインターフェースを実装しているため、io.ReadAllなどを使用してファイルの中身を取得することもできます。

ファイルの中身を取得した後は、defer src.Close()でファイルをクローズします。

外部 API への画像の POST

外部 API に画像を POST する際は、multipart/form-data形式でリクエストを送信する必要があります。Go 言語では、標準ライブラリのmime/multipartパッケージを使用してmultipart.Writerを作成し、リクエストボディを構築します。

   body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)

    part, err := writer.CreateFormFile("image_file", file.Filename)
    if err != nil {
        return types.ErrInternalServer
    }

    _, err = io.Copy(part, src)
    if err != nil {
        return types.ErrInternalServer
    }

    err = writer.Close()
    if err != nil {
        return types.ErrInternalServer
    }

    req, err := http.NewRequest("POST", "https://example.com", body)
    if err != nil {
        return types.ErrInternalServer
    }

    req.Header.Set("Content-Type", writer.FormDataContentType())
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return types.ErrInternalServer
    }
    defer resp.Body.Close()

bodyにはリクエストボディを格納するバッファを作成します。

次に、multipart.NewWriter(body)でマルチパート形式のリクエストボディを構築するmultipart.Writerオブジェクトを作成します。

writer.CreateFormFile("image_file", file.Filename)は、フォームフィールド名を"image_file"とし、ファイル名を file.Filename とするファイルパートを作成します。 そのファイルパートに、io.Copy(part, src)でファイルの中身をコピーします。 writer.Close()をすることで、最後のboundaryを書き込みます。

その後は、http.NewRequestでリクエストを作成し、req.Header.Set("Content-Type", writer.FormDataContentType())でリクエストヘッダーにContent-Typeを設定します。writer.FormDataContentType()は、multipart.Writerのboundaryを含むContent-Typeを返します。

気をつけたこと

外部 API に画像を送信する際は、エラーが発生する可能性があるためリトライ処理を実装しました。

   for tries < MaxLimit {

        // リクエスト構築
        req, err = r.buildRequest(file, fileName, hurl)
        if err != nil {
            return nil, err
        }

        // リクエスト処理
        body, err = r.doRequest(ctx, req)
        if err != nil {
            file.Seek(0, 0)
            tries++
            continue
        }
    }

リトライ処理で気を付けるべき点は、一度リクエストを送信すると画像ファイルが読み込まれた状態になるため、リトライ時にファイルのポインタを先頭に戻す必要がある点です。 ファイルポインタを戻さないと、リトライ時にファイルの中身が空になってしまいます。 multipart.Fileio.Seekerインターフェースを実装しているため、ファイルポインタを先頭に戻すには、file.Seek(0, 0)を使用することができます。

まとめ

今回は、Go 言語でmultipart/form-data形式を使って画像を受け取り、外部 API に送信する方法を紹介しました。echo フレームワークとmime/multipartパッケージを利用することで、画像の受け取りや送信が簡単に実装できます。しかし、まずmultipart/form-data形式のリクエストの仕組みを理解することが重要だと感じました。また、リトライ処理を実装する際には、リクエストボディを元の状態に戻すことを忘れないようにしましょう。