Elixir生成PDF后云端上传失败及Plug.Conn.NotSentError问题排查
问题描述
本地已成功生成PDF文件,但调用store函数上传时触发「Error uploading file」错误,同时出现以下报错:
[error] GenServer #PID<0.143080.0> terminating ** (Plug.Conn.NotSentError) a response was neither set nor sent from the connection
项目中XLSX文件的上传及下载提示功能运行正常,仅PDF模块存在该问题。
相关代码实现
PDF处理函数
defp handle_export_pdf(template_name, report_name, data) do filename = "#{report_name}.pdf" tmp_dir = "#{System.tmp_dir()}" dir_path = "#{tmp_dir}/#{filename}" with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)}, {:write_file, :ok} <- {:write_file, generate_pdf(data, report_name, dir_path)}, {:store_on_cloud, {:ok, file}} <- {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})}, {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do {:ok, file} else {:create_dir, _error} -> {:error, "Error creating directory"} {:write_file, _error} -> {:error, "Error writing data to file"} {:store_on_cloud, _error} -> {:error, "Error uploading file"} {:delete_file, _error} -> {:error, "Error deleting file"} end end
PDF生成函数
def generate_pdf(data, title, path) do html = Sneeze.render([ :html, [ :body, %{ style: style(%{ "font-family" => "Helvetica", "font-size" => "20pt" }) }, render_header(title), render_table(data) ] ]) {:ok, filename} = PdfGenerator.generate(html, page_size: "A3", shell_params: ["--dpi", "300"]) File.rename(filename, path) :ok end
完整控制器代码
defmodule EpmsWeb.ReportExportController do use EpmsWeb, :controller alias Epms.Delivery alias Epms.Assets alias Epms.Repo alias Epms.HandOffFileUploader alias Elixlsx.Workbook alias Elixlsx.Sheet @report_names %{ "a" => "Title A", "b" => "Title B", "c" => "Title C", } def export_report(conn, %{ "file_type" => file_type, "frequency_type" => frequency_type, "from_date" => from_date, "to_date" => to_date, "template_name" => template_name }) do report_name = @report_names[template_name] assigns = fetch_report_data(conn, template_name, file_type) filename_result = case file_type do "xlsx" -> handle_export_xlsx(template_name, report_name, assigns) "pdf" -> handle_export_pdf(template_name, report_name, assigns) _ -> {:error, :unsupported_file_type} end case filename_result do {:ok, filename} -> conn |> redirect(external: HandOffFileUploader.url(filename, signed: true)) |> halt() _ -> conn |> put_flash(:error, "Unsupported file type or error in generating report.") |> halt() end end defp fetch_report_data(conn, template_name, file_type) do case {template_name, file_type} do {"a", "xlsx"} -> Epms.ExportsHelper.a.get_rows(conn) {"b", "xlsx"} -> Epms.ExportsHelper.b.get_rows(conn) {"c", "xlsx"} -> Epms.ExportsHelper.c.get_rows(conn) {"a", "pdf"} -> Epms.ExportsHelper.a.get_rows(conn) {"b", "pdf"} -> Epms.ExportsHelper.b.get_rows(conn) {"c", "pdf"} -> Epms.ExportsHelper.c.get_rows(conn) # Add more cases for different file types as needed _ -> %{} end end defp handle_export_xlsx(template_name, report_name, data) do # Use the filename_for function to generate a filename filename = "#{report_name}.xlsx" tmp_dir = "#{System.tmp_dir()}" dir_path = "#{tmp_dir}/#{filename}" with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)}, {:write_file, :ok} <- {:write_file, write_data_to_file(template_name, dir_path, data)}, {:store_on_cloud, {:ok, file}} <- {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})}, {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do {:ok, file} else {:create_dir, _error} -> {:error, "Error creating directory"} {:write_file, _error} -> {:error, "Error writing data to file"} {:store_on_cloud, _error} -> {:error, "Error uploading file"} {:delete_file, _error} -> {:error, "Error deleting file"} end end defp write_data_to_file(report_template, path, data) do report_codes = Enum.map(@report_names, fn {code, _name} -> code end) if report_template in report_codes do write_xlsx(path, data) end end defp handle_export_pdf(template_name, report_name, data) do filename = "#{report_name}.pdf" tmp_dir = "#{System.tmp_dir()}" dir_path = "#{tmp_dir}/#{filename}" with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)}, {:write_file, :ok} <- {:write_file, generate_pdf(data, report_name, dir_path)}, {:store_on_cloud, {:ok, file}} <- {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})}, {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do {:ok, file} else {:create_dir, _error} -> {:error, "Error creating directory"} {:write_file, _error} -> {:error, "Error writing data to file"} {:store_on_cloud, _error} -> {:error, "Error uploading file"} {:delete_file, _error} -> {:error, "Error deleting file"} end end defp write_xlsx(path, rows) do # Assuming `rows` is a list of lists, where each inner list represents a row in the sheet max_columns = Enum.max_by(rows, &length/1) |> length # Generate a map with the same width for all columns uniform_col_widths = 1..max_columns |> Enum.map(&{&1, 20}) |> Enum.into(%{}) # Write the xlsx file sheet1 = %Sheet{name: "Export", rows: rows, col_widths: uniform_col_widths} workbook = %Workbook{sheets: [sheet1]} workbook |> Elixlsx.write_to(path) :ok end defp format_date_to_string(date) when is_nil(date), do: "" defp format_date_to_string(%Date{} = date), do: Date.to_string(date) # PDF GENERATION def generate_pdf(data, title, path) do html = Sneeze.render([ :html, [ :body, %{ style: style(%{ "font-family" => "Helvetica", "font-size" => "20pt" }) }, render_header(title), render_table(data) ] ]) {:ok, filename} = PdfGenerator.generate(html, page_size: "A3", shell_params: ["--dpi", "300"]) File.rename(filename, path) :ok end defp style(style_map) do style_map |> Enum.map(fn {key, value} -> "#{key}: #{value}" end) |> Enum.join(";") end defp render_header(title) do date = DateTime.utc_now() date_string = "#{date.year}/#{date.month}/#{date.day}" [ :div, %{ style: style(%{ "display" => "flex", "flex-direction" => "column", "align-items" => "flex-start", }) }, [ :div, %{ style: style(%{ "display" => "inline-block", "margin-top" => "10pt" }) }, [ :h1, %{ style: style(%{ "font-size" => "16pt", "margin-top" => "0pt", "padding-top" => "0pt" }) }, title ], [ :h3, %{ style: style(%{ "font-size" => "14pt", }) }, date_string ] ] ] end defp render_table(data) do table = [ :table, %{ style: style(%{ "border" => "1px solid black", "border-collapse" => "collapse", "width" => "100%" }) }, ] rows = Enum.map(data, &render_row/1) table ++ rows end defp render_row(data) do row = [ :tr, %{ style: style(%{ "border" => "1px solid black", "border-collapse" => "collapse" }) }, ] items = Enum.map(data, &render_items/1) row ++ items end defp render_items(item) do [ :td, %{ style: style(%{ "border" => "1px solid black", "border-collapse" => "collapse", "padding" => "5pt" }) }, [ :span, %{ style: style(%{ "font-size" => "12pt", "margin-top" => "0pt", "padding-top" => "0pt" }) }, item ], ] end end
问题排查与解决
1. 核心问题定位
Plug.Conn.NotSentError说明控制器未正确返回响应,结合PDF上传失败的情况,主要原因有两个:
- PDF生成/上传流程中抛出未捕获的异常,导致流程中断,无法走到响应返回逻辑
generate_pdf函数存在隐性错误,虽然本地文件看似生成成功,但实际有异常被忽略
2. 具体修复步骤
步骤1:修复generate_pdf的错误处理
当前generate_pdf中File.rename可能失败,但直接返回:ok,会导致with语句误以为文件写入成功,后续上传时文件可能不存在或损坏。修改为:
def generate_pdf(data, title, path) do html = Sneeze.render([ :html, [ :body, %{ style: style(%{ "font-family" => "Helvetica", "font-size" => "20pt" }) }, render_header(title), render_table(data) ] ]) with {:ok, filename} <- PdfGenerator.generate(html, page_size: "A3", shell_params: ["--dpi", "300"]), :ok <- File.rename(filename, path) do :ok else error -> error end end
这样重命名失败时,错误会传递到handle_export_pdf的write_file阶段,返回明确的错误信息。
步骤2:添加文件存在性检查与错误详情
在handle_export_pdf中增加文件存在校验,并打印错误详情,明确问题环节:
defp handle_export_pdf(template_name, report_name, data) do filename = "#{report_name}.pdf" tmp_dir = "#{System.tmp_dir()}" dir_path = "#{tmp_dir}/#{filename}" with {:create_dir, :ok} <- {:create_dir, File.mkdir_p!(tmp_dir)}, {:write_file, :ok} <- {:write_file, generate_pdf(data, report_name, dir_path)}, {:file_exists, true} <- {:file_exists, File.exists?(dir_path)}, {:store_on_cloud, {:ok, file}} <- {:store_on_cloud, HandOffFileUploader.store({dir_path, filename})}, {:delete_file, :ok} <- {:delete_file, File.rm(dir_path)} do {:ok, file} else {:create_dir, _error} -> {:error, "Error creating directory"} {:write_file, error} -> {:error, "Error writing data to file: #{inspect(error)}"} {:file_exists, false} -> {:error, "Generated PDF file not found at #{dir_path}"} {:store_on_cloud, error} -> {:error, "Error uploading file: #{inspect(error)}"} {:delete_file, _error} -> {:error, "Error deleting file"} end end
步骤3:捕获控制器中的异常
在export_report中添加全局异常捕获,避免因未处理的异常导致NotSentError:
def export_report(conn, params) do try do %{ "file_type" => file_type, "frequency_type" => frequency_type, "from_date" => from_date, "to_date" => to_date, "template_name" => template_name } = params report_name = @report_names[template_name] assigns = fetch_report_data(conn, template_name, file_type) filename_result = case file_type do "xlsx" -> handle_export_xlsx(template_name, report_name, assigns) "pdf" -> handle_export_pdf(template_name, report_name, assigns) _ -> {:error, :unsupported_file_type} end case filename_result do {:ok, filename} -> conn |> redirect(external: HandOffFileUploader.url(filename, signed: true)) |> halt() _ -> conn |> put_flash(:error, "Unsupported file type or error in generating report.") |> halt() end rescue e -> conn |> put_flash(:error, "Report generation failed: #{inspect(e)}") |> halt() end end
步骤4:检查上传器的MIME类型支持
确认HandOffFileUploader是否配置了PDF的MIME类型(application/pdf),XLSX能正常上传说明上传器已支持对应类型,需确保PDF类型未被拦截。
3. 验证方法
修改代码后重新测试:
- 如果返回「Generated PDF file not found」,检查临时目录权限或路径是否正确
- 如果返回上传错误详情,根据信息调整上传器配置
- 如果不再出现
NotSentError,说明异常已被正确捕获处理
内容的提问来源于stack exchange,提问作者Jonathan Talbot
相关产品推荐
相关产品推荐

