mmag

ハマったことメモなど

リバースプロキシ(Nginx)がToo Many Redirects

おはようございます。

SSLの終端はNginxで、その後ろにWebアプリがいる、みたいなよくある構成をやっていたら、「リダイレクトが多すぎます」というようなエラーが出ました。前にも同じような構成をつくったことがあったので、こんなところでハマるとは、という感じでしたが、設定ファイルから1行抜けているのが原因でした。ポカミス。

upstream app {
    server 127.0.0.1:4000;
}

# For WebSocket
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 80;
    listen [::]:80;
    server_name MYDOMAIN.COM;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name MYDOMAIN.COM;

    ssl_certificate     /etc/letsencrypt/live/MYDOMAIN.COM/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/MYDOMAIN.COM/privkey.pem;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;  # これ忘れてた
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;

        proxy_pass http://app;
    }
}

http → httpshttpshttps → ... というリダイレクトがされていたので、443の設定がトチってるんじゃのーと調べていたら見つかったわけです。

/socketへのリクエストのときにしか必要ない設定もあるので、その辺を整理して、あとSSLのランクみたいなの測るやつでAくらいにしときます。


追記

ホントに原因これなのかなという気がしてきた。合わせて使ってるcloudflareの設定が疑わしい。

PhoenixでモデルのSchemaに無い属性をレスポンスのJSONに含める

はいっこんばんは。

PhoenixJSON APIを書いてて迷ったことがあったので書きます。見落としている簡単なやり方があるんじゃないかと。

前提

UserモデルとPostモデルがあり、UserPostをlikeできるとします。postsテーブルのカラムはidtitlebodyがあり、likesテーブルにはiduser_idpost_idがあるとします。タイムスタンプは省きます。

defmodule User do
  use App.Web, :model

  schema "users" do
    field :name, :string

    has_many :likes, Like
  end
end

defmodule Post do
  use App.Web, :model

  schema "posts" do
    field :title, :string
    field :body, :text

    has_many :likes, Like
  end
end

defmodule Like do
  use App.Web, :model

  schema "likes" do
    belongs_to :user, User
    belongs_to :post, Post
  end
end

Postの一覧にLike済みかを含める

GET /postsされたら何を返すかという話なのですが、schemaにある属性をそのまま返すと以下のようになります。

{
  "posts": [
    {"id": 1, "title": "Hello", "body": "Lorem ipsum"}, ...
  ]
}

post_view.exはこんな感じ。

defmodule PostView do
  use App.Web, :view

  def render("index.json", %{posts: posts} do
    %{posts: render_many(posts, PostView, "post.json")}
  end

  def render("show.json", %{post: post} do
    %{posts: render_one(post, PostView, "post.json")}
  end

  def render("post.json", %{post: post}) do
    %{id: post.id, title: post.title, body: post.body}
  end
end

ここで、自分がLike済みか(liked)をここに含めたいと考えたとき、どうするでしょうか。リクエストヘッダにあるトークンからcurrent_userを判定しているものとして、以下のようにしたいと。

{
  "posts": [
    {"id": 1, "title": "Hello", "body": "Lorem ipsum", "liked": true}, ...
  ]
}

EctoでDBから取得

そもそも始めに、EctoでPostを取ってくるときに、一緒にlike済みかも取得するにはどうすればいいでしょうか。多分こんな。

Post
|> join(:left, [p], l in Like, l.post_id == p.id and l.user_id == ^current_user.id)
|> select([p, l], %{post: p, liked: not is_nil(l.id)})

Repo.allすると、以下の様なMapのリストが返ってきます。

[%{post: %Post{...}, liked: true}, %{post: %Post{...}, liked: false}, ... ]

ControllerからViewへ

mix phoenix.gen.jsonとかすると、Controllerには以下のようなコードが生成されます。

render(conn, "index.json", posts: posts)

すると上に書いたViewで

%{posts: render_many(posts, PostView, "post.json")}

の結果がJSONにされてレスポンスとなります。render_many/4のドキュメントによると、以下の2つがザックリ同じだそうです。

render_many(posts, PostView, "post.json")

Enum.map posts, fn post ->
  render(PostView, "show.json", post: post)
end

render/3の結果はPostView.render("show.json", post: post)となります。余談ですが、render/3の第3引数のキーである:postPostView.__resource__から来ていて、こちらで指定する場合はrender_many(posts, PostView, "post.json", as: :foo)と書きます。

このままのコードでは、レスポンスJSONの内容に、post.*しか入れることができません。つまり、postsスキーマに含まれていないlikedを入れることができません。ではどこにlikedを入れ込んでいけるか辿っていくと、Controllerのrender(conn, "index.json", posts: posts)の第3引数まで遡ります。ここにpostsしか渡していないためlikedが返せないということになります。ではEctoでDBから取ってきたこれらをまとめて雑にdataとしてぶち込みます。

# data = [%{post: %Post{...}, liked: true}, %{post: %Post{...}, liked: false}, ... ]
render(conn, "index.json", data: data)

Viewを変更

Controllerでrender/3に与える引数を変えたのでViewも修正する必要があります。

defmodule PostView do
  use App.Web, :view

  def render("index.json", %{data: data} do
    %{posts: render_many(data, PostView, "post.json", as: :data)}
  end

  def render("show.json", %{data: data} do
    %{posts: render_one(data, PostView, "post.json", as: :data)}
  end

  def render("post.json", %{data: %{post: post, liked: liked}}) do
    %{id: post.id, title: post.title, body: post.body, liked: liked}
  end
end

これでなんとかlikedが入り、当初の希望通りのJSONが返せます。

所感

ここまでやって、MVCの中を駆けまわっていたり、結合が密な感じがして、なんか面倒くさくないですか、という気持ちがちょっとだけ。mixタスクで生成したコードから出発してるからそう感じるだけかもしれませんし、Viewの中でもっと関数定義してパターンマッチすれば、いくつもlikedみたいなものが増えてもそれほど煩雑にならないのかもしれませんが。あとEctoで取ってきたものをそのままViewに投げているので、依存しているように見えてるのかもしませんね。ただ、なんか見落としてんじゃねーかなと思う午前1時半。

はやくElixirConf 2016の録画が観たいという気持ち

もう時間は深夜1時を回っていて寝なきゃいけないのに、こんなツイートを見てウォーッ!!となってしまった。

でもまだ1本もアップされておらずしゅんとしていたらこんなまとめがあるじゃないか。

github.com

寝なきゃいけないのに。