请求与响应 #

一、请求对象 #

1.1 Conn结构 #

elixir
%Plug.Conn{
  host: "localhost",
  method: "GET",
  path_info: ["users", "1"],
  request_path: "/users/1",
  params: %{"id" => "1"},
  query_string: "page=1",
  req_headers: [{"content-type", "application/json"}],
  req_cookies: %{"session" => "abc123"},
  body_params: %{"name" => "John"},
  assigns: %{},
  status: nil,
  resp_headers: [],
  resp_cookies: %{},
  state: :unset
}

1.2 请求信息 #

elixir
def show(conn, params) do
  host = conn.host
  method = conn.method
  path = conn.request_path
  query = conn.query_string
  headers = conn.req_headers

  json(conn, %{
    host: host,
    method: method,
    path: path,
    query: query
  })
end

二、请求参数 #

2.1 参数来源 #

elixir
def create(conn, params) do
  path_params = conn.path_params
  query_params = conn.query_params
  body_params = conn.body_params
  all_params = conn.params

  json(conn, %{
    path: path_params,
    query: query_params,
    body: body_params,
    all: all_params
  })
end

2.2 路径参数 #

elixir
get "/users/:id", UserController, :show
get "/posts/:post_id/comments/:id", CommentController, :show
elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)
  render(conn, :show, user: user)
end

2.3 查询参数 #

elixir
def index(conn, params) do
  page = Map.get(params, "page", "1")
  search = Map.get(params, "search", "")

  users = Hello.Accounts.list_users(page: page, search: search)
  render(conn, :index, users: users)
end

2.4 表单参数 #

elixir
def create(conn, %{"user" => user_params}) do
  case Hello.Accounts.create_user(user_params) do
    {:ok, user} ->
      redirect(conn, to: ~p"/users/#{user.id}")

    {:error, changeset} ->
      render(conn, :new, changeset: changeset)
  end
end

2.5 JSON参数 #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      json(conn, %{data: user})

    {:error, changeset} ->
      json(conn, %{errors: format_errors(changeset)})
  end
end

三、请求头 #

3.1 获取请求头 #

elixir
def show(conn, _params) do
  content_type = Plug.Conn.get_req_header(conn, "content-type")
  authorization = Plug.Conn.get_req_header(conn, "authorization")
  user_agent = Plug.Conn.get_req_header(conn, "user-agent")

  json(conn, %{
    content_type: content_type,
    authorization: authorization,
    user_agent: user_agent
  })
end

3.2 所有请求头 #

elixir
def headers(conn, _params) do
  headers = Enum.into(conn.req_headers, %{})
  json(conn, headers)
end

四、Cookie #

4.1 获取Cookie #

elixir
def show(conn, _params) do
  session_id = conn.cookies["session_id"]
  preferences = conn.cookies["preferences"]

  json(conn, %{
    session_id: session_id,
    preferences: preferences
  })
end

4.2 设置Cookie #

elixir
def set_preference(conn, %{"theme" => theme}) do
  conn
  |> put_resp_cookie("theme", theme, max_age: 365 * 24 * 60 * 60)
  |> json(%{theme: theme})
end

4.3 删除Cookie #

elixir
def clear_preference(conn, _params) do
  conn
  |> delete_resp_cookie("theme")
  |> json(%{message: "Preference cleared"})
end

五、Session #

5.1 获取Session #

elixir
def show(conn, _params) do
  user_id = Plug.Conn.get_session(conn, :user_id)
  user = Hello.Accounts.get_user(user_id)

  render(conn, :show, user: user)
end

5.2 设置Session #

elixir
def create(conn, %{"email" => email, "password" => password}) do
  case Hello.Accounts.authenticate_user(email, password) do
    {:ok, user} ->
      conn
      |> Plug.Conn.put_session(:user_id, user.id)
      |> put_flash(:info, "Logged in successfully")
      |> redirect(to: ~p"/")

    {:error, _} ->
      conn
      |> put_flash(:error, "Invalid credentials")
      |> render(:new)
  end
end

5.3 清除Session #

elixir
def delete(conn, _params) do
  conn
  |> Plug.Conn.clear_session()
  |> put_flash(:info, "Logged out successfully")
  |> redirect(to: ~p"/")
end

六、响应类型 #

6.1 HTML响应 #

elixir
def index(conn, _params) do
  users = Hello.Accounts.list_users()
  render(conn, :index, users: users)
end

6.2 JSON响应 #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)
  json(conn, %{data: user})
end

6.3 文本响应 #

elixir
def health(conn, _params) do
  text(conn, "OK")
end

6.4 文件下载 #

elixir
def download(conn, %{"id" => id}) do
  file = Hello.Files.get_file!(id)

  conn
  |> put_resp_content_type(file.content_type)
  |> send_download({:binary, file.content}, filename: file.name)
end

6.5 无内容响应 #

elixir
def delete(conn, %{"id" => id}) do
  {:ok, _} = Hello.Accounts.delete_user(id)
  send_resp(conn, :no_content, "")
end

七、响应头 #

7.1 设置响应头 #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)

  conn
  |> put_resp_header("x-request-id", generate_request_id())
  |> put_resp_header("cache-control", "public, max-age=3600")
  |> put_resp_header("x-content-type-options", "nosniff")
  |> render(:show, user: user)
end

7.2 CORS头 #

elixir
def index(conn, _params) do
  conn
  |> put_resp_header("access-control-allow-origin", "*")
  |> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE")
  |> put_resp_header("access-control-allow-headers", "content-type, authorization")
  |> json(%{data: []})
end

7.3 安全头 #

elixir
pipeline :browser do
  plug :put_secure_browser_headers
end

默认安全头:

text
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-xss-protection: 1; mode=block

八、状态码 #

8.1 成功状态码 #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      conn
      |> put_status(:created)
      |> json(%{data: user})

    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> json(%{errors: format_errors(changeset)})
  end
end

8.2 错误状态码 #

elixir
def show(conn, %{"id" => id}) do
  case Hello.Accounts.get_user(id) do
    nil ->
      conn
      |> put_status(:not_found)
      |> json(%{error: "User not found"})

    user ->
      json(conn, %{data: user})
  end
end

8.3 重定向状态码 #

elixir
def redirect_permanent(conn, _params) do
  conn
  |> put_status(:moved_permanently)
  |> redirect(to: ~p"/new-path")
end

def redirect_temporary(conn, _params) do
  conn
  |> put_status(:found)
  |> redirect(to: ~p"/temp-path")
end

九、内容协商 #

9.1 Accepts Plug #

elixir
pipeline :api do
  plug :accepts, ["json"]
end

pipeline :browser do
  plug :accepts, ["html"]
end

9.2 多格式支持 #

elixir
defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :api do
    plug :accepts, ["json", "xml"]
  end

  scope "/api", HelloWeb.Api do
    pipe_through :api

    resources "/users", UserController
  end
end
elixir
defmodule HelloWeb.Api.UserController do
  use HelloWeb, :controller

  def index(conn, _params) do
    users = Hello.Accounts.list_users()

    case get_format(conn) do
      "json" -> json(conn, %{data: users})
      "xml" -> render_xml(conn, users)
    end
  end

  defp render_xml(conn, users) do
    xml = """
    <?xml version="1.0" encoding="UTF-8"?>
    <users>
      #{Enum.map(users, &user_xml/1)}
    </users>
    """

    conn
    |> put_resp_content_type("application/xml")
    |> send_resp(200, xml)
  end

  defp user_xml(user) do
    "<user><id>#{user.id}</id><name>#{user.name}</name></user>"
  end
end

十、文件上传 #

10.1 处理上传 #

elixir
def upload(conn, %{"file" => upload}) do
  %Plug.Upload{
    filename: filename,
    content_type: content_type,
    path: temp_path
  } = upload

  case save_file(temp_path, filename) do
    {:ok, file_path} ->
      json(conn, %{path: file_path, filename: filename})

    {:error, reason} ->
      conn
      |> put_status(:unprocessable_entity)
      |> json(%{error: reason})
  end
end

defp save_file(temp_path, filename) do
  upload_dir = Application.app_dir(:hello, "priv/uploads")
  File.mkdir_p(upload_dir)

  dest_path = Path.join(upload_dir, filename)
  File.cp(temp_path, dest_path)

  {:ok, dest_path}
end

10.2 多文件上传 #

elixir
def upload_multiple(conn, %{"files" => files}) do
  results =
    files
    |> Enum.map(fn upload ->
      %Plug.Upload{filename: filename, path: temp_path} = upload
      save_file(temp_path, filename)
    end)

  json(conn, %{results: results})
end

10.3 文件验证 #

elixir
def upload(conn, %{"file" => upload}) do
  with :ok <- validate_size(upload),
       :ok <- validate_type(upload) do
    save_and_respond(conn, upload)
  else
    {:error, reason} ->
      conn
      |> put_status(:unprocessable_entity)
      |> json(%{error: reason})
  end
end

defp validate_size(%Plug.Upload{path: path}) do
  case File.stat(path) do
    {:ok, %{size: size}} when size <= 10_000_000 -> :ok
    _ -> {:error, "File too large (max 10MB)"}
  end
end

defp validate_type(%Plug.Upload{content_type: type}) do
  allowed = ["image/jpeg", "image/png", "image/gif", "application/pdf"]

  if type in allowed do
    :ok
  else
    {:error, "Invalid file type"}
  end
end

十一、流式响应 #

11.1 流式数据 #

elixir
def stream(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_chunked(200)
  |> stream_data()
end

defp stream_data(conn) do
  Enum.reduce(1..100, conn, fn i, conn ->
    chunk(conn, "Line #{i}\n")
  end)
end

11.2 Server-Sent Events #

elixir
def sse(conn, _params) do
  conn
  |> put_resp_content_type("text/event-stream")
  |> send_chunked(200)
  |> send_events()
end

defp send_events(conn) do
  Enum.reduce(1..100, conn, fn i, conn ->
    event = "data: Message #{i}\n\n"
    chunk(conn, event)
  end)
end

十二、总结 #

12.1 请求处理 #

操作 函数
获取参数 conn.params
获取请求头 get_req_header/2
获取Cookie conn.cookies
获取Session get_session/2

12.2 响应生成 #

操作 函数
HTML响应 render/3
JSON响应 json/2
文本响应 text/2
文件下载 send_download/3
状态码 put_status/2
响应头 put_resp_header/3

12.3 下一步 #

现在你已经了解了请求与响应,接下来让我们学习 视图基础,深入了解视图和模板!

最后更新:2026-03-28