Skip to content

Commit 1e5b464

Browse files
authored
Revert "Breaking change -- Add support for cloudevent function signatures (#29)" (#32)
This reverts commit 1989552. The functions framework buildpack has a bug where it pulls from master (instead of from the latest pinned release) for vendored builds, and as this change is breaking, it prevents deployment. Fixes #30, https://b.corp.google.com/issues/159732630
1 parent 1989552 commit 1e5b464

File tree

5 files changed

+145
-240
lines changed

5 files changed

+145
-240
lines changed

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ handling logic.
9595
)
9696

9797
func main() {
98-
if err := funcframework.RegisterHTTPFunction("/", hello.HelloWorld); err != nil {
99-
log.Fatalf("funcframework.RegisterHTTPFunction: %v\n", err)
100-
}
98+
funcframework.RegisterHTTPFunction("/", hello.HelloWorld)
10199
// Use PORT environment variable, or default to 8080.
102100
port := "8080"
103101
if envPort := os.Getenv("PORT"); envPort != "" {
@@ -176,11 +174,12 @@ func eventFunction(ctx context.Context, e myEventType){
176174
> Note that the first parameter to a function that handles events has to be `context.Context`
177175
and the type of second parameter needs to be a type of an unmarshallable event.
178176

179-
# Enable CloudEvents
177+
# Enable Cloud Events
180178

181-
The Functions Framework provides support for unmarshalling an incoming
182-
CloudEvents payload into a `cloudevents.Event` object. These will be passed as
183-
arguments to your function when it receives a request.
179+
The Functions Framework can unmarshal to custom structs, and provides support for
180+
unmarshalling an incoming [CloudEvents](http://cloudevents.io) payload to a
181+
`cloudevents.Event` object. These will be passed as arguments to your function when it receives a request.
182+
Note that your function must use the event-style function signature.
184183

185184
```golang
186185
func CloudEventsFunction(ctx context.Context, e cloudevents.Event) {

funcframework/framework.go

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"strings"
3030

3131
"cloud.google.com/go/functions/metadata"
32-
cloudevents "github.com/cloudevents/sdk-go/v2"
3332
)
3433

3534
const (
@@ -38,25 +37,20 @@ const (
3837
errorStatus = "error"
3938
)
4039

41-
var (
42-
handler = http.DefaultServeMux
43-
)
44-
4540
// RegisterHTTPFunction registers fn as an HTTP function.
46-
func RegisterHTTPFunction(path string, fn func(http.ResponseWriter, *http.Request)) error {
47-
return registerHTTPFunction(path, fn, handler)
41+
func RegisterHTTPFunction(path string, fn interface{}) {
42+
fnHTTP, ok := fn.(func(http.ResponseWriter, *http.Request))
43+
if !ok {
44+
panic("expected function to have signature func(http.ResponseWriter, *http.Request)")
45+
}
46+
registerHTTPFunction(path, fnHTTP, http.DefaultServeMux)
4847
}
4948

5049
// RegisterEventFunction registers fn as an event function. The function must have two arguments, a
5150
// context.Context and a struct type depending on the event, and return an error. If fn has the
52-
// wrong signature, RegisterEventFunction returns an error.
53-
func RegisterEventFunction(path string, fn interface{}) error {
54-
return registerEventFunction(path, fn, handler)
55-
}
56-
57-
// RegisterCloudEventFunction registers fn as an cloudevent function.
58-
func RegisterCloudEventFunction(ctx context.Context, path string, fn func(context.Context, cloudevents.Event)) error {
59-
return registerCloudEventFunction(ctx, path, fn, handler)
51+
// wrong signature, RegisterEventFunction panics.
52+
func RegisterEventFunction(path string, fn interface{}) {
53+
registerEventFunction(path, fn, http.DefaultServeMux)
6054
}
6155

6256
// Start serves an HTTP server with registered function(s).
@@ -65,11 +59,10 @@ func Start(port string) error {
6559
if os.Getenv("K_SERVICE") == "" {
6660
fmt.Println("Serving function...")
6761
}
68-
69-
return http.ListenAndServe(":"+port, handler)
62+
return http.ListenAndServe(":"+port, nil)
7063
}
7164

72-
func registerHTTPFunction(path string, fn func(http.ResponseWriter, *http.Request), h *http.ServeMux) error {
65+
func registerHTTPFunction(path string, fn func(http.ResponseWriter, *http.Request), h *http.ServeMux) {
7366
h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
7467
// TODO(b/111823046): Remove following once Cloud Functions does not need flushing the logs anymore.
7568
// Force flush of logs after every function trigger.
@@ -82,14 +75,15 @@ func registerHTTPFunction(path string, fn func(http.ResponseWriter, *http.Reques
8275
}()
8376
fn(w, r)
8477
})
85-
return nil
8678
}
8779

88-
func registerEventFunction(path string, fn interface{}, h *http.ServeMux) error {
89-
err := validateEventFunction(fn)
90-
if err != nil {
91-
return err
92-
}
80+
func registerEventFunction(path string, fn interface{}, h *http.ServeMux) {
81+
defer func() {
82+
if r := recover(); r != nil {
83+
fmt.Fprintf(os.Stderr, "Validation panic: %v\n\n%s", r, debug.Stack())
84+
}
85+
}()
86+
validateEventFunction(fn)
9387
h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
9488
if os.Getenv("K_SERVICE") != "" {
9589
// Force flush of logs after every function trigger when running on GCF.
@@ -103,44 +97,36 @@ func registerEventFunction(path string, fn interface{}, h *http.ServeMux) error
10397
}()
10498
handleEventFunction(w, r, fn)
10599
})
106-
return nil
107-
}
108-
109-
func registerCloudEventFunction(ctx context.Context, path string, fn func(context.Context, cloudevents.Event), h *http.ServeMux) error {
110-
p, err := cloudevents.NewHTTP()
111-
if err != nil {
112-
return fmt.Errorf("failed to create protocol: %v", err)
113-
}
114-
115-
handleFn, err := cloudevents.NewHTTPReceiveHandler(ctx, p, fn)
116-
117-
if err != nil {
118-
return fmt.Errorf("failed to create handler: %v", err)
119-
}
120-
121-
h.Handle(path, handleFn)
122-
return nil
123100
}
124101

125-
func validateEventFunction(fn interface{}) error {
102+
func validateEventFunction(fn interface{}) {
126103
ft := reflect.TypeOf(fn)
127104
if ft.NumIn() != 2 {
128-
return fmt.Errorf("expected function to have two parameters, found %d", ft.NumIn())
105+
panic(fmt.Sprintf("expected function to have two parameters, found %d", ft.NumIn()))
129106
}
130107
var err error
131108
errorType := reflect.TypeOf(&err).Elem()
132109
if ft.NumOut() != 1 || !ft.Out(0).AssignableTo(errorType) {
133-
return fmt.Errorf("expected function to return only an error")
110+
panic("expected function to return only an error")
134111
}
135112
var ctx context.Context
136113
ctxType := reflect.TypeOf(&ctx).Elem()
137114
if !ctxType.AssignableTo(ft.In(0)) {
138-
return fmt.Errorf("expected first parameter to be context.Context")
115+
panic("expected first parameter to be context.Context")
139116
}
140-
return nil
141117
}
142118

143-
func getLegacyEvent(r *http.Request, body []byte) (*metadata.Metadata, interface{}, error) {
119+
func isStructuredCloudEvent(r *http.Request) bool {
120+
ceReqHeaders := []string{"Ce-Type", "Ce-Specversion", "Ce-Source", "Ce-Id"}
121+
for _, h := range ceReqHeaders {
122+
if _, ok := r.Header[http.CanonicalHeaderKey(h)]; ok {
123+
return true
124+
}
125+
}
126+
return false
127+
}
128+
129+
func getLegacyCloudEvent(r *http.Request, body []byte) (*metadata.Metadata, interface{}, error) {
144130
// Handle legacy events' "data" and "context" fields.
145131
event := struct {
146132
Data interface{} `json:"data"`
@@ -150,7 +136,7 @@ func getLegacyEvent(r *http.Request, body []byte) (*metadata.Metadata, interface
150136
return nil, nil, err
151137
}
152138

153-
// If there is no "data" payload, this isn't a legacy event, but that's okay.
139+
// If there is no "data" payload, this isn't a legacy cloud event, but that's okay.
154140
if event.Data == nil {
155141
return nil, nil, nil
156142
}
@@ -181,12 +167,18 @@ func handleEventFunction(w http.ResponseWriter, r *http.Request, fn interface{})
181167
return
182168
}
183169

184-
// Legacy events have data and an associated metadata, so parse those and run if present.
185-
if metadata, data, err := getLegacyEvent(r, body); err != nil {
186-
writeHTTPErrorResponse(w, http.StatusBadRequest, crashStatus, fmt.Sprintf("Error: %s, parsing legacy event: %s", err.Error(), string(body)))
170+
// Structured cloud events contain the context in the header, so we need to parse that out.
171+
if isStructuredCloudEvent(r) {
172+
runStructuredCloudEvent(w, r, body, fn)
173+
return
174+
}
175+
176+
// Legacy cloud events (e.g. pubsub) have data and an associated metadata, so parse those and run if present.
177+
if metadata, data, err := getLegacyCloudEvent(r, body); err != nil {
178+
writeHTTPErrorResponse(w, http.StatusBadRequest, crashStatus, fmt.Sprintf("Error: %s, parsing legacy cloud event: %s", err.Error(), string(body)))
187179
return
188180
} else if data != nil && metadata != nil {
189-
runLegacyEvent(w, r, metadata, data, fn)
181+
runLegacyCloudEvent(w, r, metadata, data, fn)
190182
return
191183
}
192184

@@ -195,7 +187,44 @@ func handleEventFunction(w http.ResponseWriter, r *http.Request, fn interface{})
195187
return
196188
}
197189

198-
func runLegacyEvent(w http.ResponseWriter, r *http.Request, m *metadata.Metadata, data, fn interface{}) {
190+
func runStructuredCloudEvent(w http.ResponseWriter, r *http.Request, body []byte, fn interface{}) {
191+
// Parse the request to extract the context and the body for the data.
192+
event := make(map[string]interface{})
193+
event["data"] = string(body)
194+
for k, v := range r.Header {
195+
k = strings.ToLower(k)
196+
if !strings.HasPrefix(k, "ce-") {
197+
continue
198+
}
199+
k = strings.TrimPrefix(k, "ce-")
200+
if len(v) != 1 {
201+
writeHTTPErrorResponse(w, http.StatusBadRequest, crashStatus, fmt.Sprintf("Too many header values: %s", k))
202+
return
203+
}
204+
var mapVal map[string]interface{}
205+
if err := json.Unmarshal([]byte(v[0]), &mapVal); err != nil {
206+
// If there's an error, represent the field as the string from the header. Errors will be caught by the event constructor if present.
207+
event[k] = v[0]
208+
} else {
209+
// Otherwise, represent the unmarshalled map value.
210+
event[k] = mapVal
211+
}
212+
}
213+
214+
// We don't want any escaping to happen here.
215+
var buf bytes.Buffer
216+
enc := json.NewEncoder(&buf)
217+
enc.SetEscapeHTML(false)
218+
err := enc.Encode(event)
219+
if err != nil {
220+
writeHTTPErrorResponse(w, http.StatusBadRequest, crashStatus, fmt.Sprintf("Unable to construct event %v: %s", event, err.Error()))
221+
return
222+
}
223+
224+
runUserFunction(w, r, buf.Bytes(), fn)
225+
}
226+
227+
func runLegacyCloudEvent(w http.ResponseWriter, r *http.Request, m *metadata.Metadata, data, fn interface{}) {
199228
var buf bytes.Buffer
200229
enc := json.NewEncoder(&buf)
201230
enc.SetEscapeHTML(false)

0 commit comments

Comments
 (0)