Flickr の写真ファイルを取得できなくなった

no extension

Flickr の写真ファイルを取得できなくなった

私は自作ツール(公開してるけど,自分専用ツール)を使って Flickr にアップしている写真を Mastodon と Bluesky に自動投稿してるんだけど,昨日くらいからエラーで投稿できなくなった。 どうも Flickr サイトから写真データをダウンロードする際に 429 (Too Many Requests) が返っているようだ。

とりあえず自動更新処理を止めて調べ始めたのだが curl コマンドだと写真ファイルを普通に取れるのね。 ブラウザで URL を直接叩いても問題ない。 一度に複数処理するのがダメなのかと意図的に遅延させてみたけどダメ。 他にも色々弄ってみたんだけど,結局よく分からなかったので,自力で調べるのは諦め GitHub Copilot に調べさせてみた。 以下が調査結果:

可能性として一番濃いのは「Go のHTTP/2クライアント実装の指紋で弾かれている」ケースです。

今回こちらで確認した事実は以下です。

  1. sample.go をそのまま go run sample.go すると HTTP/2.0 429。
  2. 同じコードでも GODEBUG=http2client=0(HTTP/2無効)だと成功。
  3. net/http のカスタム Transport(実質HTTP/1.1)でも 200 OK。
  4. curl は HTTP/2 でも 200。

この組み合わせから、単純なURL間違いや fetch のバグよりも、次が考えやすいです。

  1. CDN/WAFのbot判定差
    GoのHTTP/2(TLS/ALPN, SETTINGS, HPACK, ヘッダー順序など)のシグネチャが curl と異なり、レート制限・Bot制限に触れることがあります。

  2. User-Agent 偽装との不整合
    sample.go で User-Agent: curl/8.18.0 を送っていますが、実際のTLS/HTTP2実装はcurlではないため、判定が厳しい環境だと不自然と見なされることがあります。

  3. 経路差・POP差による429
    同じURLでも、接続先POPやタイミング次第で429/200が揺れることがあります。go と curl でDNS解決・接続経路が微妙に変わる場合もあります。

実運用での対策候補です。

  1. 対象ホストだけHTTP/1.1にフォールバックする(最も効く可能性が高い)
  2. User-Agent を偽装せず、実アプリ名にする
  3. 429時に Retry-After を見て指数バックオフ再試行
  4. 必要なら Referer など相手側が期待するヘッダーを追加(規約順守前提)

ちなみに上のやり取りで使ってる sample.go の中身はこんな感じ。

package main

import (
	"context"
	"fmt"

	"github.com/goark/fetch"
)

func main() {
	u, err := fetch.URL("https://live.staticflickr.com/65535/55582060788_9bf7829404_b.jpg")
	if err != nil {
		fmt.Printf("%+v\n", err) // %+v でエラー詳細を出力
		return
	}
	resp, err := fetch.New().GetWithContext(
		context.Background(),
		u,
		fetch.WithRequestHeaderSet("User-Agent", "curl/8.18.0"),
		fetch.WithRequestHeaderSet("Accept", "*/*"),
	)
	if err != nil {
		fmt.Printf("%+v\n", err) // %+v ステータスコードを含むエラー詳細を出力
		return
	}
	defer resp.Close()

	fmt.Println("HTTP Protocol:", resp.Proto())
}

これを実行すると以下の出力になる。

$ go run sample.go | jq .
{
  "Type": "*errs.Error",
  "Err": {
    "Type": "*errors.errorString",
    "Msg": "invalid HTTP request"
  },
  "Context": {
    "function": "github.com/goark/fetch.(*client).GetWithContext",
    "url": "https://live.staticflickr.com/65535/55582060788_9bf7829404_b.jpg"
  },
  "Cause": {
    "Type": "*errs.Error",
    "Err": {
      "Type": "*fmt.wrapError",
      "Msg": "bad HTTP status: status 429",
      "Cause": {
        "Type": "*errors.errorString",
        "Msg": "bad HTTP status"
      }
    },
    "Context": {
      "function": "github.com/goark/fetch.(*client).fetch",
      "protocol": "HTTP/2.0",
      "status": 429
    }
  }
}

429 が返っているのがお分かりだろうか。

goark/fetch は標準の net/http をラップした自作パッケージで,fetch.New() で引数を省略するとデフォルトの http.Client{} インスタンスを使うようになっている。 ここを,たとえば

resp, err := fetch.New(fetch.WithHTTPClient(
    &http.Client{
        Transport: &http.Transport{
            ForceAttemptHTTP2: false, // HTTP/2 の無効化
            TLSNextProto:      map[string]func(string, *tls.Conn) http.RoundTripper{}, // TLSNextProto を空にする
        },
    },
)).GetWithContext(
    context.Background(),
    u,
    fetch.WithRequestHeaderSet("User-Agent", "curl/8.18.0"),
    fetch.WithRequestHeaderSet("Accept", "*/*"),
)

とすれば HTTP/1.1 専用のクライアントになる。 これを使ってリクエストを発行し,ステータス 200 (OK) で写真ファイルがダウンロードできることを確認できた。

強制的に HTTP/1.1 を使うのはイケてない気がするが,素の net/http の HTTP/2 挙動を調査するのは大変だし,今回は対症療法ということで HTTP/1.1 専用クライアントを生成する方法を採用した。

毎回構造体をゴリゴリ書くのは面倒なので goark/fetch に HTTP/1.1 専用のクライアントを生成するオプションを追加して対応した。 さらにこれを自作ツールに組み込んで修正完了。

#flickr #photography 街路樹 https://www.flickr.com/photos/spiegel/55594508941/

[image or embed]

— Spiegel with お散歩カメラ (@photos.baldanders.info) 2026年10月17日 20:10

よしよし。 ちゃんと動いてるな。

Go 1.27 から http.Response.Body の中身を読み捨てなくてよくなった

昔から Go 特有のイディオムとして http.Response.Body を Close() する前に中身を読み捨てる必要があったが,1.27 からは http.Transport がいい感じに処理してくれるため,明示的に読み捨てる必要がなくなった。

Starting in Go 1.27, net/http.Transport automatically does everything we have implemented in the code above: it will asynchronously drain an unread HTTP/1 response body for up to 50 ms or 256 KB. It also implements additional optimizations; for example, when a response contains the Connection: close header, draining the body is guaranteed to be wasted work, and it therefore skips doing so. Note that this behavior might evolve in future versions of Go, as other possible enhancements become apparent.

As a general best practice, we now recommend that users simply close their HTTP bodies once they no longer need them, like in our original example:

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    return resp.StatusCode, nil
}

Go is all about simplicity. Let your code stay clean and simple, and let net/http deal with the complexities on your behalf.

これを goark/fetch に対応しようか悩んだのだが,このパッケージはまだ 1.26 対応のままなので,今回は諦めた。 1.28 が出て 1.26 がサポートから外れたら,改めて対応することにしよう。

Go はシンプルが正義!

ATProto ライブラリ indigo の古い API が Deprecated になってた

自作ツールで Bluesky に投稿するために Go 用の ATProto ライブラリ bluesky-social/indigo パッケージを使っているのだが,今の最新版(頼むからバージョンタグを付けてくれ!)をインポートしたら初期から使ってる API が deprecated になっていた。

今のところは見なかったことにしてバージョンを戻している(笑)

でも,近いうちに時間を取って対応しないと。 他にやりたいことが山積みなんだがな。

参考図書

thumbs
プログラミング言語Go - 丸善出版 理工・医学・人文社会科学の専門書出版社
明解で効率的なプログラムを描くための書。 柴田 芳樹 訳
 
Release 2016-06-15
評価     

[Comment] 著者のひとりは,あの “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!

Powered by linkcard

photo
Go言語 100Tips ありがちなミスを把握し、実装を最適化する impress top gearシリーズ
Teiva Harsanyi (著), 柴田 芳樹 (著)
インプレス 2023-08-18 (Release 2023-08-18)
Kindle版
B0CFL1DK8Q (ASIN)
評価     

[Comment] 版元で PDF 版を購入可能。事実上の Effective Go とも言える充実の内容。オリジナルは敢えてタイトルに “tips” という単語を入れるのを避けたのに邦題が「100 Tips」とかなっていて,原作者がお怒りとの噂(あくまで噂)

reviewed by Spiegel on 2023-08-18 (powered by PA-APIv5)

photo
Go言語で学ぶ並行プログラミング 他言語にも適用できる原則とベストプラクティス impress top gearシリーズ
James Cutajar (著), 柴田 芳樹 (著)
インプレス 2024-12-04 (Release 2024-12-04)
Kindle版
B0DNYMMBBQ (ASIN)
評価     

[Comment] 読書会のために購入。インプレス社の本は Kindle 版より版元で PDF 版を買うのがオススメ。「並行処理」について原理的な解説から丁寧に書かれている。 Go で解説されているが Go 以外の言語でも応用できる。

reviewed by Spiegel on 2025-01-25 (powered by PA-APIv5)