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

  1. Pass the full path to a regular file (e.g. /alloc/logs/web.stdout.0) instead of a directory.
  2. Use the file listing endpoint (GET /v1/client/fs/ls) first to enumerate files, then stream a concrete file entry.
  3. 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

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


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/ee17ab6e7b5d5918. Report an issue: GitHub.