Skip to content

Attachments in v5 returning 404 and type application/json #2929

Closed

Description

@henderb

This code works in v4, but when run in v5, the browser says ERR_INVALID_RESPONSE

package main

import (
	"encoding/csv"
	"errors"
	"log"
	"net/http"
	"os"
	"strconv"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type Item struct {
	Name  string
	Price float64
	Cost  float64
}

func main() {
	e := echo.New()
	e.Use(middleware.RequestLogger())
	e.Use(middleware.Gzip())

	e.GET("/data.csv", func(c echo.Context) error {

		items := []Item{
			Item{
				Name:  "Item 1",
				Price: 10.99,
				Cost:  4.63,
			},
			Item{
				Name:  "Item 2",
				Price: 20.99,
				Cost:  12.82,
			},
		}

		f, err := os.CreateTemp("/tmp/", "data-*.csv")
		if err != nil {
			e.Logger.Error("Could not create data temp file", err)
			return c.HTML(500, "Could not create data temp file")
		}
		defer f.Close()

		ic := csv.NewWriter(f)
		ic.Write([]string{
			"Name",
			"Price",
			"Cost"})
		for _, item := range items {
			row := []string{
				item.Name,
				strconv.FormatFloat(item.Price, 'f', 2, 64),
				strconv.FormatFloat(item.Cost, 'f', 3, 64)}

			ic.Write(row)
		}
		ic.Flush()

		return c.Attachment(f.Name(), "data.csv")
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

v5:

package main

import (
	"encoding/csv"
	"errors"
	"log"
	"net/http"
	"os"
	"strconv"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

type Item struct {
	Name  string
	Price float64
	Cost  float64
}

func main() {
	e := echo.New()
	e.Use(middleware.RequestLogger())
	e.Use(middleware.Gzip())

	e.GET("/data.csv", func(c *echo.Context) error {

		items := []Item{
			Item{
				Name:  "Item 1",
				Price: 10.99,
				Cost:  4.63,
			},
			Item{
				Name:  "Item 2",
				Price: 20.99,
				Cost:  12.82,
			},
		}

		f, err := os.CreateTemp("/tmp/", "data-*.csv")
		if err != nil {
			e.Logger.Error("Could not create data temp file", err)
			return c.HTML(500, "Could not create data temp file")
		}
		defer f.Close()

		ic := csv.NewWriter(f)
		ic.Write([]string{
			"Name",
			"Price",
			"Cost"})
		for _, item := range items {
			row := []string{
				item.Name,
				strconv.FormatFloat(item.Price, 'f', 2, 64),
				strconv.FormatFloat(item.Cost, 'f', 3, 64)}

			ic.Write(row)
		}
		ic.Flush()

		return c.Attachment(f.Name(), "data.csv")
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

Activity

  1. aldas commented on Mar 30, 2026

    @aldas
    Contributor

    in v5 the default e.Filesystem is set to CWD current working directory and file related methods need to take account that filepath need adjusting

    You can set your filesystem to /tmp with

    e.Filesystem = os.DirFS("/tmp")

    and when you serve the file the path must be trimmed from /tmp prefix

    return c.Attachment(filepath.Base(f.Name()), "data.csv")
    package main
    
    import (
    	"encoding/csv"
    	"errors"
    	"log"
    	"net/http"
    	"os"
    	"path/filepath"
    	"strconv"
    
    	"github.com/labstack/echo/v5"
    	"github.com/labstack/echo/v5/middleware"
    )
    
    type Item struct {
    	Name  string
    	Price float64
    	Cost  float64
    }
    
    func main() {
    	e := echo.New()
    	e.Filesystem = os.DirFS("/tmp")
    
    	e.Use(middleware.RequestLogger())
    	e.Use(middleware.Gzip())
    
    	e.GET("/data.csv", func(c *echo.Context) error {
    
    		items := []Item{
    			Item{
    				Name:  "Item 1",
    				Price: 10.99,
    				Cost:  4.63,
    			},
    			Item{
    				Name:  "Item 2",
    				Price: 20.99,
    				Cost:  12.82,
    			},
    		}
    
    		f, err := os.CreateTemp("/tmp/", "data-*.csv")
    		if err != nil {
    			e.Logger.Error("Could not create data temp file", err)
    			return c.HTML(500, "Could not create data temp file")
    		}
    		defer f.Close()
    
    		ic := csv.NewWriter(f)
    		ic.Write([]string{
    			"Name",
    			"Price",
    			"Cost"})
    		for _, item := range items {
    			row := []string{
    				item.Name,
    				strconv.FormatFloat(item.Price, 'f', 2, 64),
    				strconv.FormatFloat(item.Cost, 'f', 3, 64)}
    
    			ic.Write(row)
    		}
    		ic.Flush()
    
    		return c.Attachment(filepath.Base(f.Name()), "data.csv")
    	})
    
    	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
    		log.Fatal(err)
    	}
    }
  2. aldas commented on Mar 30, 2026

    @aldas
  3. aldas commented on Mar 30, 2026

    @aldas
    Contributor

    I will investigate if we can similar tricks with default filesystem so dir prefix would not be that big annoyance

  4. aldas commented on Mar 31, 2026

    @aldas
    Contributor

    @henderb I have modified defaultFS behavior so that Open can handle absolute path file names have matching prefix as filesystem. The default filesystem will still be initialized to CWD but now it easier to create fs can mimic more os.Open behavior.

    so if you would change fs to, you example would work.

    e.Filesystem = echo.NewDefaultFS("/tmp")

    I'll release new version today

  5. henderb commented on Mar 31, 2026

    @henderb
    Author

    thanks, since I'm not sure what else might depend on the default filesystem path, I've changed my temp file creation from /tmp/ to tmp/ and it seems happy.

  6. aldas commented on Mar 31, 2026

    @aldas
    Contributor

    @henderb /tmp is cleaned on each restart under linux. but anyway you should not forget that files you are creating need to be removed somehow or you will run out of free space or inodes.

  7. henderb commented on Mar 31, 2026

    @henderb
    Author

    good point.

    I think I'll be ok since fly.io doesn't keep files written outside mounted volumes across instance boots. Mine is scaling to zero very often. But could be a problem for later.

    On dev, I'm rebuilding the container often enough or could have docker-compose map it somewhere in /tmp/my-app/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions