feat: add ctx ssevent; fix: superfluous response.WriteHeader

This commit is contained in:
loveuer 2024-04-19 16:56:40 +08:00
parent 436264117c
commit 479c4eef57
6 changed files with 154 additions and 6 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea
.vscode
.DS_Store
xtest

3
app.go
View File

@ -19,8 +19,6 @@ var (
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
mimePlain = []string{"text/plain"}
)
type App struct {
@ -176,7 +174,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
serveError(c, errorHandler)
}
c.writermem.WriteHeaderNow()
return
}
if httpMethod != http.MethodConnect && rPath != "/" {

21
ctx.go
View File

@ -3,7 +3,9 @@ package nf
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/loveuer/nf/internal/sse"
"io"
"mime/multipart"
"net"
@ -281,7 +283,7 @@ func (c *Ctx) SendString(data string) error {
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
c.SetHeader("Content-Type", "text/plain")
return c.writer.Write([]byte(fmt.Sprintf(format, values...)))
return c.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Ctx) JSON(data interface{}) error {
@ -296,6 +298,23 @@ func (c *Ctx) JSON(data interface{}) error {
return nil
}
func (c *Ctx) SSEvent(event string, data interface{}) error {
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Transfer-Encoding", "chunked")
return sse.Encode(c.writer, sse.Event{Event: event, Data: data})
}
func (c *Ctx) Flush() error {
if f, ok := c.writer.(http.Flusher); ok {
f.Flush()
return nil
}
return errors.New("http.Flusher is not implemented")
}
func (c *Ctx) RawWriter() http.ResponseWriter {
return c.writer
}

106
internal/sse/sse-encoder.go Normal file
View File

@ -0,0 +1,106 @@
package sse
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)
// Server-Sent Events
// W3C Working Draft 29 October 2009
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
const ContentType = "text/event-stream"
var contentType = []string{ContentType}
var noCache = []string{"no-cache"}
var fieldReplacer = strings.NewReplacer(
"\n", "\\n",
"\r", "\\r")
var dataReplacer = strings.NewReplacer(
"\n", "\ndata:",
"\r", "\\r")
type Event struct {
Event string
Id string
Retry uint
Data interface{}
}
func Encode(writer io.Writer, event Event) error {
w := checkWriter(writer)
writeId(w, event.Id)
writeEvent(w, event.Event)
writeRetry(w, event.Retry)
return writeData(w, event.Data)
}
func writeId(w stringWriter, id string) {
if len(id) > 0 {
w.WriteString("id:")
fieldReplacer.WriteString(w, id)
w.WriteString("\n")
}
}
func writeEvent(w stringWriter, event string) {
if len(event) > 0 {
w.WriteString("event:")
fieldReplacer.WriteString(w, event)
w.WriteString("\n")
}
}
func writeRetry(w stringWriter, retry uint) {
if retry > 0 {
w.WriteString("retry:")
w.WriteString(strconv.FormatUint(uint64(retry), 10))
w.WriteString("\n")
}
}
func writeData(w stringWriter, data interface{}) error {
w.WriteString("data:")
switch kindOfData(data) {
case reflect.Struct, reflect.Slice, reflect.Map:
err := json.NewEncoder(w).Encode(data)
if err != nil {
return err
}
w.WriteString("\n")
default:
dataReplacer.WriteString(w, fmt.Sprint(data))
w.WriteString("\n\n")
}
return nil
}
func (r Event) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return Encode(w, r)
}
func (r Event) WriteContentType(w http.ResponseWriter) {
header := w.Header()
header["Content-Type"] = contentType
if _, exist := header["Cache-Control"]; !exist {
header["Cache-Control"] = noCache
}
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

24
internal/sse/writer.go Normal file
View File

@ -0,0 +1,24 @@
package sse
import "io"
type stringWriter interface {
io.Writer
WriteString(string) (int, error)
}
type stringWrapper struct {
io.Writer
}
func (w stringWrapper) WriteString(str string) (int, error) {
return w.Writer.Write([]byte(str))
}
func checkWriter(writer io.Writer) stringWriter {
if w, ok := writer.(stringWriter); ok {
return w
} else {
return stringWrapper{writer}
}
}

View File

@ -3,6 +3,7 @@ package nf
import (
"bufio"
"io"
"log"
"net"
"net/http"
)
@ -60,7 +61,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code {
if w.Written() {
// todo: debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
log.Printf("[NF] WARNING: Headers were already written. Wanted to override status code %d with %d", w.status, code)
return
}
w.status = code
@ -102,7 +103,7 @@ func (w *responseWriter) Size() int {
}
func (w *responseWriter) Written() bool {
return w.size != noWritten
return w.size != noWritten || w.status != 0
}
// Hijack implements the http.Hijacker interface.