fix: read subprocess output as raw bytes to avoid locale encoding errors
Build and Deploy / build-and-deploy (push) Successful in 1m32s

readProcessWithExitCode returns stdout/stderr as String, which forces GHC
to decode raw bytes using the current locale encoding. In a Docker
container with C locale (default), non-ASCII UTF-8 bytes (like 0xE2)
cause 'invalid argument' errors.

Replace with readProcessBytes which uses createProcess + hSetBinaryMode
to capture stdout/stderr as raw lazy ByteStrings, bypassing locale
decoding entirely. JSON is decoded directly from raw bytes via A.decode.
This commit is contained in:
2026-05-20 14:53:11 -04:00
parent ebeb52212d
commit 8d1ff6eb86
+24 -11
View File
@@ -50,7 +50,8 @@ import qualified Roux.Html as Html
import qualified Roux.RecipeIndex as Idx
import Roux.SchemaOrg (SchemaOrgRecipe (..), parseSchemaOrgRecipe, schemaOrgToCooklang)
import System.Exit (ExitCode (..))
import System.Process (readProcessWithExitCode)
import System.IO (hSetBinaryMode)
import System.Process (CreateProcess (..), StdStream (CreatePipe), createProcess, proc, waitForProcess)
-- | Read the full request body as a lazy ByteString.
readRequestBody :: Wai.Request -> IO LB.ByteString
@@ -234,6 +235,22 @@ parseFormBody body =
in toEnum hexVal : go rest
go (c : rest) = c : go rest
-- | Run a subprocess and capture stdout/stderr as raw bytes (no locale decoding).
readProcessBytes :: FilePath -> [String] -> IO (ExitCode, LB.ByteString, LB.ByteString)
readProcessBytes cmd args = do
let process =
(proc cmd args)
{ std_out = CreatePipe
, std_err = CreatePipe
}
(Just outH, Just errH, _, ph) <- createProcess process
hSetBinaryMode outH True
hSetBinaryMode errH True
out <- LB.hGetContents outH
err <- LB.hGetContents errH
ec <- waitForProcess ph
return (ec, out, err)
-- | Locate the scrape-recipe script: check PATH first, then relative to project.
findScrapeScript :: IO FilePath
findScrapeScript = do
@@ -250,23 +267,19 @@ runImportPipeline recipeDir url = do
-- Step 1: locate and run the scraper
script <- findScrapeScript
putStrLn $ "[roux] importing from " <> T.unpack url <> " using script " <> script
(exitCode, stdout, stderr) <-
readProcessWithExitCode
script
["--pretty", T.unpack url]
""
(exitCode, stdoutBytes, stderrBytes) <-
readProcessBytes script ["--pretty", T.unpack url]
case exitCode of
ExitFailure _ -> do
putStrLn $ "[roux] scrape-recipe exited with code " <> show exitCode <> " for " <> T.unpack url
putStrLn $ "[roux] scrape-recipe stderr: " <> stderr
putStrLn $ "[roux] scrape-recipe stderr: " <> T.unpack (TE.decodeUtf8 (LB.toStrict stderrBytes))
return $
Left $
Html.ImportError $
"Could not scrape URL (" <> url <> "). The scraper exited with: " <> T.pack stderr
"Could not scrape URL (" <> url <> "). The scraper exited with: " <> TE.decodeUtf8 (LB.toStrict stderrBytes)
ExitSuccess -> do
-- Step 2: decode stdout (String) as JSON Value
let jsonBytes = LB.fromStrict (TE.encodeUtf8 (T.pack stdout))
jsonVal = A.decode jsonBytes :: Maybe A.Value
-- Step 2: decode stdout (raw bytes) as JSON Value
let jsonVal = A.decode stdoutBytes :: Maybe A.Value
case jsonVal of
Nothing -> do
putStrLn $ "[roux] failed to decode JSON from scrape-recipe stdout for " <> T.unpack url