feat: add ctx ssevent; fix: superfluous response.WriteHeader
This commit is contained in:
parent
436264117c
commit
479c4eef57
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
xtest
|
3
app.go
3
app.go
@ -19,8 +19,6 @@ var (
|
|||||||
|
|
||||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||||
|
|
||||||
mimePlain = []string{"text/plain"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@ -176,7 +174,6 @@ func (a *App) handleHTTPRequest(c *Ctx) {
|
|||||||
serveError(c, errorHandler)
|
serveError(c, errorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.writermem.WriteHeaderNow()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if httpMethod != http.MethodConnect && rPath != "/" {
|
if httpMethod != http.MethodConnect && rPath != "/" {
|
||||||
|
21
ctx.go
21
ctx.go
@ -3,7 +3,9 @@ package nf
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/loveuer/nf/internal/sse"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
@ -281,7 +283,7 @@ func (c *Ctx) SendString(data string) error {
|
|||||||
|
|
||||||
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
||||||
c.SetHeader("Content-Type", "text/plain")
|
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 {
|
func (c *Ctx) JSON(data interface{}) error {
|
||||||
@ -296,6 +298,23 @@ func (c *Ctx) JSON(data interface{}) error {
|
|||||||
return nil
|
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 {
|
func (c *Ctx) RawWriter() http.ResponseWriter {
|
||||||
return c.writer
|
return c.writer
|
||||||
}
|
}
|
||||||
|
106
internal/sse/sse-encoder.go
Normal file
106
internal/sse/sse-encoder.go
Normal 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
24
internal/sse/writer.go
Normal 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}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package nf
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@ -60,7 +61,7 @@ func (w *responseWriter) reset(writer http.ResponseWriter) {
|
|||||||
func (w *responseWriter) WriteHeader(code int) {
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
if code > 0 && w.status != code {
|
if code > 0 && w.status != code {
|
||||||
if w.Written() {
|
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
|
return
|
||||||
}
|
}
|
||||||
w.status = code
|
w.status = code
|
||||||
@ -102,7 +103,7 @@ func (w *responseWriter) Size() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Written() bool {
|
func (w *responseWriter) Written() bool {
|
||||||
return w.size != noWritten
|
return w.size != noWritten || w.status != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack implements the http.Hijacker interface.
|
// Hijack implements the http.Hijacker interface.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user