Skip to content

📝 [Proposal]: Provide an effective TCP disconnect handle #4145

@bincooo

Description

@bincooo

Feature Proposal Description

First of all, I would like to thank the developers for their efforts.
In order to better support stream/sse processing, could you add a listening implementation for TCP disconnection? I have encountered many such situations where the front end actively disconnects the request, but the backend fails to detect it in a timely manner. It is only when the backend task is completed or an attempt is made to write to the stream that a read/write exception is issued.

Alignment with Express API

I have made a crude implementation, and I am not sure if there is a related implementation for this project. I would like to offer a suggestion:

package v1

import (
	"context"
	"errors"
	"net"
	"syscall"
	"time"

	"github.com/gofiber/fiber/v3"
)

type abortCtx struct {
	parent context.Context
	done   chan struct{}
	err    error
}

// 创建一个监听客户端断开的 Context
func newAbort(ctx fiber.Ctx) (context.Context, context.CancelFunc) {
	cctx := &abortCtx{
		parent: ctx.Context(),
		done:   make(chan struct{}),
	}

	loop := true
	cancel := func() {
		loop = false
		select {
		case <-cctx.done:
			// 已经关闭
		default:
			cctx.err = context.Canceled
			close(cctx.done)
		}
	}

	ch := make(chan byte, 1)
	conn := ctx.RequestCtx().Conn()
	go func() {
		for loop {
			if isClosed(conn) {
				ch <- 1
				loop = false
			}
			time.Sleep(time.Millisecond * 500)
		}
	}()

	// 后台轮询 IsAbandoned
	go func() {
		select {
		case <-cctx.parent.Done():
			select {
			case <-cctx.done:
			default:
				cctx.err = cctx.parent.Err()
				close(cctx.done)
			}
			return

		case <-ch:
			select {
			case <-cctx.done:
			default:
				cctx.err = context.Canceled
				close(cctx.done)
			}
			return

		case <-cctx.done:
			return
		}
	}()

	return cctx, cancel
}

func (c *abortCtx) Deadline() (time.Time, bool) {
	return c.parent.Deadline()
}

func (c *abortCtx) Done() <-chan struct{} {
	return c.done
}

func (c *abortCtx) Err() error {
	select {
	case <-c.done:
		return c.err
	default:
		return nil
	}
}

func (c *abortCtx) Value(key any) any {
	return c.parent.Value(key)
}

func isClosed(conn net.Conn) bool {
	// 获取底层文件描述符
	rawConn, ok := conn.(syscall.Conn)
	if !ok {
		return false
	}

	var closed bool
	rc, err := rawConn.SyscallConn()
	if err != nil {
		return false
	}

	_ = rc.Read(func(fd uintptr) bool {
		buf := make([]byte, 1)

		// 使用 MSG_PEEK:查看数据但不消费
		n, _, ioerr := syscall.Recvfrom(int(fd), buf, syscall.MSG_PEEK|syscall.MSG_DONTWAIT)
		if n == 0 && ioerr == nil {
			// EOF:对端关闭
			closed = true
		}

		if errors.Is(ioerr, syscall.ECONNRESET) {
			closed = true
		}

		// EAGAIN/EWOULDBLOCK = 没有数据但连接正常
		return true
	})

	return closed
}

HTTP RFC Standards Compliance

API Stability

Feature Examples

-

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have searched for existing issues that describe my proposal before opening this one.
  • I understand that a proposal that does not meet these guidelines may be closed without explanation.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions