Skip to content

Commit 21bc0d7

Browse files
authored
Migrated the FCM impl to the new HTTPClient API (#275)
1 parent 851c0a2 commit 21bc0d7

File tree

8 files changed

+509
-430
lines changed

8 files changed

+509
-430
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
testdata/integration_*
22
.vscode/*
3+
*~
4+

internal/http_client.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ type CreateErrFn func(r *Response) error
5656
// NewHTTPClient creates a new HTTPClient using the provided client options and the default
5757
// RetryConfig.
5858
//
59-
// The default RetryConfig retries requests on all low-level network errors as well as on HTTP
60-
// InternalServerError (500) and ServiceUnavailable (503) errors. Repeatedly failing requests are
61-
// retried up to 4 times with exponential backoff. Retry delay is never longer than 2 minutes.
62-
//
6359
// NewHTTPClient returns the created HTTPClient along with the target endpoint URL. The endpoint
6460
// is obtained from the client options passed into the function.
6561
func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClient, string, error) {
@@ -68,8 +64,18 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
6864
return nil, "", err
6965
}
7066

67+
return WithDefaultRetryConfig(hc), endpoint, nil
68+
}
69+
70+
// WithDefaultRetryConfig creates a new HTTPClient using the provided client and the default
71+
// RetryConfig.
72+
//
73+
// The default RetryConfig retries requests on all low-level network errors as well as on HTTP
74+
// InternalServerError (500) and ServiceUnavailable (503) errors. Repeatedly failing requests are
75+
// retried up to 4 times with exponential backoff. Retry delay is never longer than 2 minutes.
76+
func WithDefaultRetryConfig(hc *http.Client) *HTTPClient {
7177
twoMinutes := time.Duration(2) * time.Minute
72-
client := &HTTPClient{
78+
return &HTTPClient{
7379
Client: hc,
7480
RetryConfig: &RetryConfig{
7581
MaxRetries: 4,
@@ -81,7 +87,6 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
8187
MaxDelay: &twoMinutes,
8288
},
8389
}
84-
return client, endpoint, nil
8590
}
8691

8792
// Request contains all the parameters required to construct an outgoing HTTP request.

messaging/messaging.go

Lines changed: 48 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,12 @@ import (
2828
"time"
2929

3030
"firebase.google.com/go/internal"
31+
"google.golang.org/api/transport"
3132
)
3233

3334
const (
3435
messagingEndpoint = "https://fcm.googleapis.com/v1"
3536
batchEndpoint = "https://fcm.googleapis.com/batch"
36-
iidEndpoint = "https://iid.googleapis.com"
37-
iidSubscribe = "iid/v1:batchAdd"
38-
iidUnsubscribe = "iid/v1:batchRemove"
3937

4038
firebaseClientHeader = "X-Firebase-Client"
4139
apiFormatVersionHeader = "X-GOOG-API-FORMAT-VERSION"
@@ -104,37 +102,8 @@ var (
104102
"app instance has been unregistered; code: " + registrationTokenNotRegistered,
105103
},
106104
}
107-
108-
iidErrorCodes = map[string]struct{ Code, Msg string }{
109-
"INVALID_ARGUMENT": {
110-
invalidArgument,
111-
"request contains an invalid argument; code: " + invalidArgument,
112-
},
113-
"NOT_FOUND": {
114-
registrationTokenNotRegistered,
115-
"request contains an invalid argument; code: " + registrationTokenNotRegistered,
116-
},
117-
"INTERNAL": {
118-
internalError,
119-
"server encountered an internal error; code: " + internalError,
120-
},
121-
"TOO_MANY_TOPICS": {
122-
tooManyTopics,
123-
"client exceeded the number of allowed topics; code: " + tooManyTopics,
124-
},
125-
}
126105
)
127106

128-
// Client is the interface for the Firebase Cloud Messaging (FCM) service.
129-
type Client struct {
130-
fcmEndpoint string // to enable testing against arbitrary endpoints
131-
batchEndpoint string // to enable testing against arbitrary endpoints
132-
iidEndpoint string // to enable testing against arbitrary endpoints
133-
client *internal.HTTPClient
134-
project string
135-
version string
136-
}
137-
138107
// Message to be sent via Firebase Cloud Messaging.
139108
//
140109
// Message contains payload data, recipient information and platform-specific configuration
@@ -635,39 +604,10 @@ type ErrorInfo struct {
635604
Reason string
636605
}
637606

638-
// TopicManagementResponse is the result produced by topic management operations.
639-
//
640-
// TopicManagementResponse provides an overview of how many input tokens were successfully handled,
641-
// and how many failed. In case of failures, the Errors list provides specific details concerning
642-
// each error.
643-
type TopicManagementResponse struct {
644-
SuccessCount int
645-
FailureCount int
646-
Errors []*ErrorInfo
647-
}
648-
649-
func newTopicManagementResponse(resp *iidResponse) *TopicManagementResponse {
650-
tmr := &TopicManagementResponse{}
651-
for idx, res := range resp.Results {
652-
if len(res) == 0 {
653-
tmr.SuccessCount++
654-
} else {
655-
tmr.FailureCount++
656-
code := res["error"].(string)
657-
info, ok := iidErrorCodes[code]
658-
var reason string
659-
if ok {
660-
reason = info.Msg
661-
} else {
662-
reason = unknownError
663-
}
664-
tmr.Errors = append(tmr.Errors, &ErrorInfo{
665-
Index: idx,
666-
Reason: reason,
667-
})
668-
}
669-
}
670-
return tmr
607+
// Client is the interface for the Firebase Cloud Messaging (FCM) service.
608+
type Client struct {
609+
*fcmClient
610+
*iidClient
671611
}
672612

673613
// NewClient creates a new instance of the Firebase Cloud Messaging Client.
@@ -679,27 +619,51 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error
679619
return nil, errors.New("project ID is required to access Firebase Cloud Messaging client")
680620
}
681621

682-
hc, _, err := internal.NewHTTPClient(ctx, c.Opts...)
622+
hc, _, err := transport.NewHTTPClient(ctx, c.Opts...)
683623
if err != nil {
684624
return nil, err
685625
}
686626

687627
return &Client{
628+
fcmClient: newFCMClient(hc, c),
629+
iidClient: newIIDClient(hc),
630+
}, nil
631+
}
632+
633+
type fcmClient struct {
634+
fcmEndpoint string
635+
batchEndpoint string
636+
project string
637+
version string
638+
httpClient *internal.HTTPClient
639+
}
640+
641+
func newFCMClient(hc *http.Client, conf *internal.MessagingConfig) *fcmClient {
642+
client := internal.WithDefaultRetryConfig(hc)
643+
client.CreateErrFn = handleFCMError
644+
client.SuccessFn = internal.HasSuccessStatus
645+
646+
version := fmt.Sprintf("fire-admin-go/%s", conf.Version)
647+
client.Opts = []internal.HTTPOption{
648+
internal.WithHeader(apiFormatVersionHeader, apiFormatVersion),
649+
internal.WithHeader(firebaseClientHeader, version),
650+
}
651+
652+
return &fcmClient{
688653
fcmEndpoint: messagingEndpoint,
689654
batchEndpoint: batchEndpoint,
690-
iidEndpoint: iidEndpoint,
691-
client: hc,
692-
project: c.ProjectID,
693-
version: "fire-admin-go/" + c.Version,
694-
}, nil
655+
project: conf.ProjectID,
656+
version: version,
657+
httpClient: client,
658+
}
695659
}
696660

697661
// Send sends a Message to Firebase Cloud Messaging.
698662
//
699663
// The Message must specify exactly one of Token, Topic and Condition fields. FCM will
700664
// customize the message for each target platform based on the arguments specified in the
701665
// Message.
702-
func (c *Client) Send(ctx context.Context, message *Message) (string, error) {
666+
func (c *fcmClient) Send(ctx context.Context, message *Message) (string, error) {
703667
payload := &fcmRequest{
704668
Message: message,
705669
}
@@ -710,36 +674,28 @@ func (c *Client) Send(ctx context.Context, message *Message) (string, error) {
710674
//
711675
// This function does not actually deliver the message to target devices. Instead, it performs all
712676
// the SDK-level and backend validations on the message, and emulates the send operation.
713-
func (c *Client) SendDryRun(ctx context.Context, message *Message) (string, error) {
677+
func (c *fcmClient) SendDryRun(ctx context.Context, message *Message) (string, error) {
714678
payload := &fcmRequest{
715679
ValidateOnly: true,
716680
Message: message,
717681
}
718682
return c.makeSendRequest(ctx, payload)
719683
}
720684

721-
// SubscribeToTopic subscribes a list of registration tokens to a topic.
722-
//
723-
// The tokens list must not be empty, and have at most 1000 tokens.
724-
func (c *Client) SubscribeToTopic(ctx context.Context, tokens []string, topic string) (*TopicManagementResponse, error) {
725-
req := &iidRequest{
726-
Topic: topic,
727-
Tokens: tokens,
728-
op: iidSubscribe,
685+
func (c *fcmClient) makeSendRequest(ctx context.Context, req *fcmRequest) (string, error) {
686+
if err := validateMessage(req.Message); err != nil {
687+
return "", err
729688
}
730-
return c.makeTopicManagementRequest(ctx, req)
731-
}
732689

733-
// UnsubscribeFromTopic unsubscribes a list of registration tokens from a topic.
734-
//
735-
// The tokens list must not be empty, and have at most 1000 tokens.
736-
func (c *Client) UnsubscribeFromTopic(ctx context.Context, tokens []string, topic string) (*TopicManagementResponse, error) {
737-
req := &iidRequest{
738-
Topic: topic,
739-
Tokens: tokens,
740-
op: iidUnsubscribe,
690+
request := &internal.Request{
691+
Method: http.MethodPost,
692+
URL: fmt.Sprintf("%s/projects/%s/messages:send", c.fcmEndpoint, c.project),
693+
Body: internal.NewJSONEntity(req),
741694
}
742-
return c.makeTopicManagementRequest(ctx, req)
695+
696+
var result fcmResponse
697+
_, err := c.httpClient.DoAndUnmarshal(ctx, request, &result)
698+
return result.Name, err
743699
}
744700

745701
// IsInternal checks if the given error was due to an internal server error.
@@ -812,49 +768,6 @@ type fcmError struct {
812768
} `json:"error"`
813769
}
814770

815-
type iidRequest struct {
816-
Topic string `json:"to"`
817-
Tokens []string `json:"registration_tokens"`
818-
op string
819-
}
820-
821-
type iidResponse struct {
822-
Results []map[string]interface{} `json:"results"`
823-
}
824-
825-
type iidError struct {
826-
Error string `json:"error"`
827-
}
828-
829-
func (c *Client) makeSendRequest(ctx context.Context, req *fcmRequest) (string, error) {
830-
if err := validateMessage(req.Message); err != nil {
831-
return "", err
832-
}
833-
834-
request := &internal.Request{
835-
Method: http.MethodPost,
836-
URL: fmt.Sprintf("%s/projects/%s/messages:send", c.fcmEndpoint, c.project),
837-
Body: internal.NewJSONEntity(req),
838-
Opts: []internal.HTTPOption{
839-
internal.WithHeader(apiFormatVersionHeader, apiFormatVersion),
840-
internal.WithHeader(firebaseClientHeader, c.version),
841-
},
842-
}
843-
844-
resp, err := c.client.Do(ctx, request)
845-
if err != nil {
846-
return "", err
847-
}
848-
849-
if resp.Status == http.StatusOK {
850-
var result fcmResponse
851-
err := json.Unmarshal(resp.Body, &result)
852-
return result.Name, err
853-
}
854-
855-
return "", handleFCMError(resp)
856-
}
857-
858771
func handleFCMError(resp *internal.Response) error {
859772
var fe fcmError
860773
json.Unmarshal(resp.Body, &fe) // ignore any json parse errors at this level
@@ -882,59 +795,3 @@ func handleFCMError(resp *internal.Response) error {
882795
}
883796
return internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg)
884797
}
885-
886-
func (c *Client) makeTopicManagementRequest(ctx context.Context, req *iidRequest) (*TopicManagementResponse, error) {
887-
if len(req.Tokens) == 0 {
888-
return nil, fmt.Errorf("no tokens specified")
889-
}
890-
if len(req.Tokens) > 1000 {
891-
return nil, fmt.Errorf("tokens list must not contain more than 1000 items")
892-
}
893-
for _, token := range req.Tokens {
894-
if token == "" {
895-
return nil, fmt.Errorf("tokens list must not contain empty strings")
896-
}
897-
}
898-
899-
if req.Topic == "" {
900-
return nil, fmt.Errorf("topic name not specified")
901-
}
902-
if !topicNamePattern.MatchString(req.Topic) {
903-
return nil, fmt.Errorf("invalid topic name: %q", req.Topic)
904-
}
905-
906-
if !strings.HasPrefix(req.Topic, "/topics/") {
907-
req.Topic = "/topics/" + req.Topic
908-
}
909-
910-
request := &internal.Request{
911-
Method: http.MethodPost,
912-
URL: fmt.Sprintf("%s/%s", c.iidEndpoint, req.op),
913-
Body: internal.NewJSONEntity(req),
914-
Opts: []internal.HTTPOption{internal.WithHeader("access_token_auth", "true")},
915-
}
916-
resp, err := c.client.Do(ctx, request)
917-
if err != nil {
918-
return nil, err
919-
}
920-
921-
if resp.Status == http.StatusOK {
922-
var result iidResponse
923-
if err := json.Unmarshal(resp.Body, &result); err != nil {
924-
return nil, err
925-
}
926-
return newTopicManagementResponse(&result), nil
927-
}
928-
929-
var ie iidError
930-
json.Unmarshal(resp.Body, &ie) // ignore any json parse errors at this level
931-
var clientCode, msg string
932-
info, ok := iidErrorCodes[ie.Error]
933-
if ok {
934-
clientCode, msg = info.Code, info.Msg
935-
} else {
936-
clientCode = unknownError
937-
msg = fmt.Sprintf("client encountered an unknown error; response: %s", string(resp.Body))
938-
}
939-
return nil, internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg)
940-
}

0 commit comments

Comments
 (0)