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
:globalor a GenServer) to ensure only one process generates a given image - Ensure
send_fileis 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:
- Label text passed to
label:andcaption:commands - 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