Skip to content

Commit fe7362b

Browse files
author
Peter Kieltyka
committed
Moved to goware org
0 parents  commit fe7362b

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

README.md

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# JSONP Go http middleware
2+
3+
JSONP is a common technique used to communicate with a JSON-serving Web Service with a
4+
Web browser over cross-domains, in place of a XHR request. There is a lot written about
5+
JSONP out there, but the tl;dr on it is a Javascript http client requesting JSONP
6+
will write a `<script>` tag to the head of a page, with the `src` to an API endpoint,
7+
with the addition of a `callback` (or `jsonp`) query parameter that represents a
8+
randomly-named listener function that will parse the request when it comes back from
9+
the server.
10+
11+
This middleware will work with anything that supports standard `http.Handler`. The code
12+
is small, so go read it, but it just buffers the response from the rest of the chain,
13+
and if its a JSON request with a callback, then it will wrap the response in the callback
14+
function before writing it to the actual response writer.
15+
16+
Any feedback is welcome and appreciated!
17+
18+
Written by [@pkieltyka](https://github.com/pkieltyka)
19+
20+
## Example
21+
22+
```go
23+
// JSONP example using Goji framework.. but anything that accepts
24+
// a http.Handler middleware chain will work
25+
package main
26+
27+
import (
28+
"log"
29+
"net/http"
30+
31+
"github.com/goware/jsonp"
32+
"github.com/unrolled/render"
33+
"github.com/zenazn/goji/web"
34+
"github.com/zenazn/goji/web/middleware"
35+
)
36+
37+
func main() {
38+
mux := web.New()
39+
render := render.New(render.Options{})
40+
41+
mux.Use(middleware.Logger)
42+
mux.Use(jsonp.Handler)
43+
44+
mux.Get("/", func(w http.ResponseWriter, r *http.Request) {
45+
data := &SomeObj{"superman"}
46+
render.JSON(w, 200, data)
47+
})
48+
49+
err := http.ListenAndServe(":4444", mux)
50+
if err != nil {
51+
log.Fatal(err)
52+
}
53+
}
54+
55+
type SomeObj struct {
56+
Name string `json:"name"`
57+
}
58+
59+
```
60+
61+
*Output:*
62+
```
63+
curl -v "http://localhost:4444/"
64+
> GET / HTTP/1.1
65+
> User-Agent: curl/7.37.1
66+
> Host: localhost:4444
67+
> Accept: */*
68+
>
69+
< HTTP/1.1 200 OK
70+
< Content-Type: application/json; charset=UTF-8
71+
< Date: Sat, 08 Nov 2014 16:05:04 GMT
72+
< Content-Length: 19
73+
<
74+
* Connection #0 to host localhost left intact
75+
{"name":"superman"}
76+
```
77+
78+
```
79+
curl -v "http://localhost:4444/?callback=X"
80+
> GET /?callback=X HTTP/1.1
81+
> User-Agent: curl/7.37.1
82+
> Host: localhost:4444
83+
> Accept: */*
84+
>
85+
< HTTP/1.1 200 OK
86+
< Content-Length: 22
87+
< Content-Type: application/javascript
88+
< Date: Sat, 08 Nov 2014 16:05:23 GMT
89+
<
90+
* Connection #0 to host localhost left intact
91+
X({"name":"superman"})
92+
```
93+
94+
95+
## Also
96+
97+
Since JSONP must always respond with a 200, as thats what the browser `<script>`
98+
tag expects, a nice pattern that is also used in the GitHub API is to put the HTTP
99+
response headers in a `"meta"` hash, and the HTTP response body in `"data"`. Like so..
100+
101+
```json
102+
JsonpCallbackFn_abc123etc({
103+
"meta": {
104+
"Status": 200,
105+
"Content-Type": "application/json",
106+
"Content-Length": "19",
107+
"etc": "etc"
108+
},
109+
"data": { "name": "yummy" }
110+
})
111+
```

example/main.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// JSONP example using Goji framework.. but anything that accepts
2+
// a http.Handler middleware chain will work
3+
package main
4+
5+
import (
6+
"log"
7+
"net/http"
8+
9+
"github.com/goware/jsonp"
10+
"github.com/unrolled/render"
11+
"github.com/zenazn/goji/web"
12+
"github.com/zenazn/goji/web/middleware"
13+
)
14+
15+
func main() {
16+
mux := web.New()
17+
render := render.New(render.Options{})
18+
19+
mux.Use(middleware.Logger)
20+
mux.Use(jsonp.Handler)
21+
22+
mux.Get("/", func(w http.ResponseWriter, r *http.Request) {
23+
data := &SomeObj{"superman"}
24+
render.JSON(w, 200, data)
25+
})
26+
27+
err := http.ListenAndServe(":4444", mux)
28+
if err != nil {
29+
log.Fatal(err)
30+
}
31+
}
32+
33+
type SomeObj struct {
34+
Name string `json:"name"`
35+
}

jsonp.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package jsonp
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
func Handler(next http.Handler) http.Handler {
13+
fn := func(w http.ResponseWriter, r *http.Request) {
14+
callback := r.URL.Query().Get("callback")
15+
if callback == "" {
16+
callback = r.URL.Query().Get("jsonp")
17+
}
18+
if callback == "" {
19+
next.ServeHTTP(w, r)
20+
return
21+
}
22+
23+
wb := NewResponseBuffer(w)
24+
next.ServeHTTP(wb, r)
25+
26+
if strings.Index(wb.Header().Get("Content-Type"), "/json") >= 0 {
27+
status := wb.Status
28+
data := wb.Body.Bytes()
29+
wb.Body.Reset()
30+
31+
resp := &jsonpResponse{
32+
Meta: map[string]interface{}{"status": status},
33+
Data: data,
34+
}
35+
for k, v := range wb.Header() {
36+
resp.Meta[strings.ToLower(k)] = v[0]
37+
}
38+
resp.Meta["content-length"] = len(data)
39+
40+
body, err := json.Marshal(resp)
41+
if err != nil {
42+
panic(err.Error())
43+
}
44+
45+
wb.Body.Write([]byte(callback + "("))
46+
wb.Body.Write(body)
47+
wb.Body.Write([]byte(")"))
48+
49+
wb.Header().Set("Content-Type", "application/javascript")
50+
wb.Header().Set("Content-Length", strconv.Itoa(wb.Body.Len()))
51+
}
52+
53+
wb.Flush()
54+
}
55+
return http.HandlerFunc(fn)
56+
}
57+
58+
type jsonpResponse struct {
59+
Meta map[string]interface{}
60+
Data interface{}
61+
}
62+
63+
func (j *jsonpResponse) MarshalJSON() ([]byte, error) {
64+
meta, err := json.Marshal(j.Meta)
65+
if err != nil {
66+
return nil, err
67+
}
68+
b := fmt.Sprintf("{\"meta\":%s,\"data\":%s}", meta, j.Data)
69+
return []byte(b), nil
70+
}
71+
72+
type responseBuffer struct {
73+
Response http.ResponseWriter // the actual ResponseWriter to flush to
74+
Status int // the HTTP response code from WriteHeader
75+
Body *bytes.Buffer // the response content body
76+
Flushed bool
77+
}
78+
79+
func NewResponseBuffer(w http.ResponseWriter) *responseBuffer {
80+
return &responseBuffer{
81+
Response: w, Status: 200, Body: &bytes.Buffer{},
82+
}
83+
}
84+
85+
func (w *responseBuffer) Header() http.Header {
86+
return w.Response.Header() // use the actual response header
87+
}
88+
89+
func (w *responseBuffer) Write(buf []byte) (int, error) {
90+
w.Body.Write(buf)
91+
return len(buf), nil
92+
}
93+
94+
func (w *responseBuffer) WriteHeader(status int) {
95+
w.Status = status
96+
}
97+
98+
func (w *responseBuffer) Flush() {
99+
if w.Flushed {
100+
return
101+
}
102+
w.Response.WriteHeader(w.Status)
103+
if w.Body.Len() > 0 {
104+
_, err := w.Response.Write(w.Body.Bytes())
105+
if err != nil {
106+
panic(err)
107+
}
108+
w.Body.Reset()
109+
}
110+
w.Flushed = true
111+
}

0 commit comments

Comments
 (0)