firebase / firebase-admin-go

Firebase Admin Go SDK
Apache License 2.0
1.12k stars 239 forks source link

Realtime Database: GetIfChanged and GetWithETag will not return etag if json.Unmarshal returns an error #568

Open ccw-morris opened 12 months ago

ccw-morris commented 12 months ago

Describe your environment

Describe the problem json.Unmarshal() is robust to schema issues - it will populate as much of the value as possible. https://pkg.go.dev/encoding/json#Unmarshal

If a JSON value is not appropriate for a given target type, or if a JSON number overflows the target type, Unmarshal skips that field and completes the unmarshaling as best it can. If no more serious errors are encountered, Unmarshal returns an UnmarshalTypeError describing the earliest such error.

However, GetWithETag treats all errors from json.Unmarshall as catastrophic failures: it returns an empty etag despite the value v being populated by json.Unmarshall(). https://github.com/firebase/firebase-admin-go/blob/74c9bd5edece6044ba365a04c32c00074006dd7c/db/ref.go#L93

GetIfChanged is similar. It returns an empty etag, and also fails to declare if the data has changed. https://github.com/firebase/firebase-admin-go/blob/74c9bd5edece6044ba365a04c32c00074006dd7c/db/ref.go#L130

The consequences are:

I would expect the methods above to return any etag in the response - even when the client has subsequently generated an error while trying to process the body of that response. This ensures that the client doesn't repeatedly download and process a message that it can't.

This seems to be caused by the HTTP client - which fails to return the response if it encounters an unmarshal error. So, neither of the two methods above are able to retrieve the etag from the response that hasn't been returned. https://github.com/firebase/firebase-admin-go/blob/74c9bd5edece6044ba365a04c32c00074006dd7c/internal/http_client.go#L170

Workaround There is a workaround - the client can use json.RawMessage to defer parsing the response. This ensures that GetIfChanged and GetWithETag will not encounter an unmarshalling error and will therefore always return the etag.

var v json.RawMessage
etag, err := ref.GetWithETag(context.Background(), &v)
if err != nil {
  // This can only be an error in the request - for example, a connection failure. Handle appropriately.
}
data := make(<your data struct>)
err = json.Unmarshal(v, &data)
if err != nil {
  // This could be one of three errors: InvalidUnmarshalError , SyntaxError, or UnmarshalTypeError.
  // InvalidUnmarshalError indicates that data is nil; SyntaxError indicates that Firebase has returned non-json data
  // UnmarshalTypeError indicates at least one schema error in the data - but data object will be populated as best it can be
  // Handle appropriately.
}