hashicorp/nomad · error
file %q is a directory
Error message
file %q is a directory
What it means
The file stream endpoint stat's the requested path and, if the result is a directory, refuses to stream it because only regular files can be streamed at an offset. The error is returned as HTTP 400 Bad Request with the requested path embedded in the message.
Source
Thrown at client/fs_endpoint.go:237
if err != nil {
code := new(int64(http.StatusInternalServerError))
if structs.IsErrUnknownAllocation(err) {
code = new(int64(http.StatusNotFound))
}
handleStreamResultError(err, code, encoder)
return
}
// Calculate the offset
fileInfo, err := fs.Stat(req.Path)
if err != nil {
handleStreamResultError(err, new(int64(http.StatusBadRequest)), encoder)
return
}
if fileInfo.IsDir {
handleStreamResultError(
fmt.Errorf("file %q is a directory", req.Path),
new(int64(http.StatusBadRequest)), encoder)
return
}
// If offsetting from the end subtract from the size
if req.Origin == "end" {
req.Offset = max(fileInfo.Size-req.Offset, 0)
}
frames := make(chan *sframer.StreamFrame, streamFramesBuffer)
errCh := make(chan error)
var buf bytes.Buffer
frameCodec := codec.NewEncoder(&buf, structs.JsonHandle)
// Create the framer
framer := sframer.NewStreamFramer(frames, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
framer.Run()
defer framer.Destroy()View on GitHub (pinned to 482b49bf1a)
Solutions
- Pass the full path to a regular file (e.g. /alloc/logs/web.stdout.0) instead of a directory.
- Use the file listing endpoint (GET /v1/client/fs/ls) first to enumerate files, then stream a concrete file entry.
- Validate the requested path is a file with the stat endpoint (GET /v1/client/fs/stat) before streaming.
Example fix
// before
r, err := a.Stream(ctx, &api.StreamFrameReq{Path: "/alloc/logs/"})
// after
info, _, err := client.AllocFS().Stat(alloc, "/alloc/logs/web.stdout.0", nil)
if err != nil || info.IsDir {
return fmt.Errorf("path is a directory or missing")
}
r, err = client.AllocFS().Stream(alloc, "/alloc/logs/web.stdout.0", "start", 0, false, nil, ctx.Done()) Defensive patterns
Strategy: validation
Validate before calling
info, _, err := client.AllocFS().Stat(alloc, path, nil)
if err != nil {
return err
}
if info.IsDir {
return fmt.Errorf("%s is a directory; use List", path)
} Type guard
func isDirStat(err error) bool {
return strings.Contains(err.Error(), "is a directory")
} Try / catch
if err := streamFile(path); err != nil {
if isDirStat(err) {
entries, lerr := client.AllocFS().List(alloc, path, nil)
if lerr == nil {
useEntries(entries)
}
return nil
}
return err
} Prevention
- Stat the path before streaming; switch to the ls endpoint for directories.
- Never pass directory paths from user input straight to the stream API.
- Build paths from ls results rather than hardcoded assumptions.
When it happens
Trigger: Calling GET /v1/client/fs/stream (or the Allocs().Logs/files API backing it) with a path parameter pointing to a directory instead of a regular file, e.g. streaming /alloc/logs/ instead of a specific log file.
Common situations: Hardcoded paths that point at directories; using the fs endpoint to browse directories when the list endpoint (/v1/client/fs/ls) is the right one; path built from user input that resolves to a directory; a file replaced by a directory after a task restart.
Related errors
- unknown task name %q
- failed to list entries: %v
- failed to convert %q to a log index: %v
- unable to read rooted allocation directory
- no servers
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/ee17ab6e7b5d5918.
Report an issue: GitHub.