Files
labelmaker/BUGS.md
2025-10-14 18:15:41 -04:00

8.6 KiB

Known Bugs and Issues

This document lists identified bugs and potential issues in the Labelmaker codebase.

Critical Issues

1. Race Condition in Image Generation

Location: lib/labelmaker_web/controllers/label_controller.ex:21-31

Status: ⚠️ UNRESOLVED

Issue: When multiple requests arrive simultaneously for the same non-existent image, both will start generating it concurrently. Additionally, send_file/3 is called immediately after starting generation without waiting for it to complete, which will cause a file-not-found error.

unless File.exists?(filepath) do
  basic_settings(options)
  |> outline_settings(options)
  |> size_settings(options)
  |> final_settings(options)
  |> generate_image()
end

conn
|> put_resp_content_type("image/png")
|> send_file(200, filepath)  # Called before generation completes!

Impact: High - Will cause 500 errors when requests race or when generation is slow

Suggested Fix:

  • Use a mutex/lock mechanism (e.g., via :global or a GenServer) to ensure only one process generates a given image
  • Ensure send_file is only called after generation completes
  • Consider using a temporary file with atomic rename to prevent serving partially-written images
# Example approach with synchronization
case ensure_image_exists(filepath, options) do
  :ok ->
    conn
    |> put_resp_content_type("image/png")
    |> send_file(200, filepath)
  {:error, reason} ->
    conn
    |> put_status(500)
    |> json(%{error: "Failed to generate image"})
end

2. Crash on Invalid Numeric Inputs

Location: lib/labelmaker_web/tools.ex:87-88, 136-137, 140-141

Status: ⚠️ UNRESOLVED

Issue: When height, width, or size parameters contain non-numeric strings (e.g., "abc", "12.5px"), String.to_integer/1 raises an ArgumentError that isn't caught, causing the entire request to crash.

defp process_height(height, _parameters) do
  height |> String.to_integer() |> max(0) |> min(Constants.max_height())
end

defp process_width(width, _parameters) do
  width |> String.to_integer() |> max(0) |> min(Constants.max_width())
end

defp calculate_preview_height(parameters) do
  size = parameters["size"] |> String.to_integer()
  # ...
end

Impact: High - User-supplied invalid input causes crashes

Suggested Fix:

defp process_height(height, _parameters) do
  case Integer.parse(height) do
    {int, _} -> int |> max(0) |> min(Constants.max_height())
    :error -> String.to_integer(Constants.defaults().height)
  end
end

defp process_width(width, _parameters) do
  case Integer.parse(width) do
    {int, _} -> int |> max(0) |> min(Constants.max_width())
    :error -> String.to_integer(Constants.defaults().width)
  end
end

defp calculate_preview_height(parameters) do
  size = case Integer.parse(parameters["size"]) do
    {int, _} -> int
    :error -> 72  # default
  end
  rows = calculate_rows(parameters["label"])
  size + size * rows
end

Moderate Issues

3. Missing Error Handling on ImageMagick Command

Location: lib/labelmaker_web/controllers/label_controller.ex:112

Status: ⚠️ UNRESOLVED

Issue: Pattern matching on {_, 0} will crash if ImageMagick returns a non-zero exit code (e.g., invalid arguments, ImageMagick not installed, permission errors, disk full).

{_, 0} = System.cmd("magick", args)

Impact: Moderate - Crashes on ImageMagick errors instead of gracefully handling

Suggested Fix:

case System.cmd("magick", args, stderr_to_stdout: true) do
  {_output, 0} ->
    :ok
  {error, code} ->
    require Logger
    Logger.error("ImageMagick failed with code #{code}: #{error}")
    {:error, :image_generation_failed}
end

Then handle the error tuple in the controller's show/2 function and return appropriate HTTP error response.

4. Potential nil Crash on Font Lookup

Location: lib/labelmaker_web/tools.ex:34-35

Status: ⚠️ UNRESOLVED

Issue: If an invalid font is passed that isn't in the font_map, the map lookup returns nil. While later validation (line 64) filters it out, if defaults somehow don't include font, nil could be passed to ImageMagick causing cryptic errors.

{:font, font} ->
  {:font, Constants.font_map()[String.downcase(font)]}

Impact: Low-Moderate - Edge case but could cause ImageMagick errors

Suggested Fix:

{:font, font} ->
  {:font, Map.get(Constants.font_map(), String.downcase(font), Constants.defaults().font)}

5. label_too_long Flag Logic Bug

Location: lib/labelmaker_web/tools.ex:40-41

Status: 🐛 CONFIRMED BUG

Issue: The label_too_long flag is calculated AFTER the label has already been truncated in process_label/1 (line 16-17). This means the flag will always be false because it's checking the length of the already-truncated label, not the original input.

# Line 16-17: Label gets truncated here
{"label", label} ->
  {"label", process_label(label)}

# Line 40-41: This checks the ALREADY TRUNCATED label
{:label_too_long, _} ->
  {:label_too_long, String.length(parameters["label"]) > Constants.max_label_length()}

Impact: Low - UI warning for long labels never appears

Suggested Fix: Check the length before truncation, or pass the original length through:

def process_parameters(parameters) do
  # Calculate label_too_long before any processing
  original_label_length = String.length(Map.get(parameters, "label", ""))

  parameters =
    Constants.defaults()
    |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
    |> Map.merge(parameters)
    |> Map.put("_original_label_length", original_label_length)
    # ... rest of processing

  # Later when setting the flag:
  {:label_too_long, _} ->
    {:label_too_long, parameters["_original_label_length"] > Constants.max_label_length()}

Low Priority Issues

6. Hash Collision Risk from inspect()

Location: lib/labelmaker_web/controllers/label_controller.ex:11-15

Status: ⚠️ UNRESOLVED

Issue: Using inspect/1 on a map for hashing is fragile. Map ordering isn't guaranteed to be consistent across Erlang/OTP versions or runtime conditions. Two identical maps could theoretically produce different inspect strings, leading to cache misses.

filename =
  options
  |> inspect()
  |> (fn str -> :crypto.hash(:sha256, str) end).()
  |> Base.encode16(case: :lower)

Impact: Low - Mostly theoretical, but could cause unnecessary cache misses

Suggested Fix: Hash a consistently-ordered representation:

filename =
  options
  |> Map.take([:label, :color, :font, :outline, :size, :width, :height, :align])
  |> Enum.sort()
  |> :erlang.term_to_binary()
  |> :crypto.hash(:sha256)
  |> Base.encode16(case: :lower)

7. Missing Type Guards on String Operations

Location: lib/labelmaker_web/tools.ex:32, 35

Status: ⚠️ UNRESOLVED

Issue: String.downcase/1 is called on align and font parameters without verifying they're strings. Given the data flow this is unlikely to be a problem, but defensive programming would add guards.

{:align, align} ->
  {:align, align |> String.downcase()}

{:font, font} ->
  {:font, Constants.font_map()[String.downcase(font)]}

Impact: Very Low - Input validation earlier in the pipeline makes this unlikely

Suggested Fix: Add type guards or convert to string first:

{:align, align} when is_binary(align) ->
  {:align, String.downcase(align)}

# Or alternatively:
{:align, align} ->
  {:align, align |> to_string() |> String.downcase()}

Fixed Issues

8. ImageMagick Property Interpolation Warning

Location: lib/labelmaker_web/controllers/label_controller.ex:50, 65, 96

Status: FIXED

Issue: Labels containing % characters (like "Test!@#$%" or URL-encoded text) caused ImageMagick to emit warnings about unknown properties like %2, %20, etc., because ImageMagick treats % as a special character for property interpolation.

Fix Applied: All % characters are now escaped to %% in:

  1. Label text passed to label: and caption: commands
  2. Metadata stored in image comments
# Lines 47-50, 62-65
escaped_label =
  options.label
  |> String.slice(0, Constants.max_label_length())
  |> String.replace("%", "%%")

# Lines 91-96
comment =
  options
  |> Map.drop([:filepath, :link])
  |> Jason.encode!()
  |> inspect()
  |> String.replace("%", "%%")

Notes

  • All critical and moderate issues should be addressed before deploying to production
  • The test suite (105 tests) currently passes but doesn't cover error conditions for many of these bugs
  • Consider adding integration tests that specifically test error handling paths