33module  ActionPushNative 
44  module  Service 
55    class  Fcm 
6-       # FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer. 
7-       # https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts 
8-       DEFAULT_TIMEOUT  =  15 . seconds 
6+       include  NetworkErrorHandling 
7+ 
8+       # Per-application HTTPX session 
9+       cattr_accessor  :httpx_sessions 
910
1011      def  initialize ( config ) 
1112        @config  =  config 
1213      end 
1314
1415      def  push ( notification ) 
15-         response  =  post_request   payload_from ( notification ) 
16-         handle_error ( response )  unless  response . code  ==  "200" 
16+         response  =  httpx_session . post ( "v1/projects/ #{ config . fetch ( :project_id ) } /messages:send" ,   json :  payload_from ( notification ) ,   headers :  {   authorization :  "Bearer  #{ access_token } "   } ) 
17+         handle_error ( response )  if  response . error 
1718      end 
1819
1920      private 
2021        attr_reader  :config 
2122
22-         def  payload_from ( notification ) 
23-           deep_compact ( { 
24-             message : { 
25-               token : notification . token , 
26-               data : notification . data  ? stringify ( notification . data )  : { } , 
27-               android : { 
28-                 notification : { 
29-                   title : notification . title , 
30-                   body : notification . body , 
31-                   notification_count : notification . badge , 
32-                   sound : notification . sound 
33-                 } , 
34-                 collapse_key : notification . thread_id , 
35-                 priority : notification . high_priority  == true  ? "high"  : "normal" 
36-               } 
37-             } . deep_merge ( notification . google_data  ? stringify_data ( notification . google_data )  : { } ) 
38-           } ) 
23+         def  httpx_session 
24+           self . class . httpx_sessions  ||= { } 
25+           self . class . httpx_sessions [ config ]  ||= build_httpx_session 
3926        end 
4027
41-         def  deep_compact ( payload ) 
42-           payload . dig ( :message ,  :android ,  :notification ) . try ( &:compact! ) 
43-           payload . dig ( :message ,  :android ) . try ( &:compact! ) 
44-           payload [ :message ] . compact! 
45-           payload 
46-         end 
28+         # FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer. 
29+         # https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts 
30+         DEFAULT_REQUEST_TIMEOUT  =  15 . seconds 
31+         DEFAULT_POOL_SIZE        =  5 
4732
48-         # FCM requires data values to be strings. 
49-         def  stringify_data ( google_data ) 
50-           google_data . tap  do  |payload |
51-             payload [ :data ]  =  stringify ( payload [ :data ] )  if  payload [ :data ] 
52-           end 
33+         def  build_httpx_session 
34+           HTTPX . 
35+             plugin ( :persistent ,  close_on_fork : true ) . 
36+             with ( timeout : {  request_timeout : config [ :request_timeout ]  || DEFAULT_REQUEST_TIMEOUT  } ) . 
37+             with ( pool_options : {  max_connections : config [ :connection_pool_size ]  || DEFAULT_POOL_SIZE  } ) . 
38+             with ( origin : "https://fcm.googleapis.com" ) 
5339        end 
5440
55-         def  stringify ( hash ) 
56-           hash . compact . transform_values ( &:to_s ) 
57-         end 
41+         concerning  :Payload  do 
42+           def  payload_from ( notification ) 
43+             deep_compact ( { 
44+               message : { 
45+                 token : notification . token , 
46+                 data : notification . data  ? stringify ( notification . data )  : { } , 
47+                 android : { 
48+                   notification : { 
49+                     title : notification . title , 
50+                     body : notification . body , 
51+                     notification_count : notification . badge , 
52+                     sound : notification . sound 
53+                   } , 
54+                   collapse_key : notification . thread_id , 
55+                   priority : notification . high_priority  == true  ? "high"  : "normal" 
56+                 } 
57+               } . deep_merge ( notification . google_data  ? stringify_data ( notification . google_data )  : { } ) 
58+             } ) 
59+           end 
5860
59-         def  post_request ( payload ) 
60-           uri   =   URI ( "https://fcm.googleapis.com/v1/projects/ #{ config . fetch ( :project_id ) } /messages:send" ) 
61-           request   =   Net :: HTTP :: Post . new ( uri ) 
62-           request [ "Authorization" ]   =   "Bearer  #{ access_token } " 
63-           request [ "Content-Type" ]    =   "application/json" 
64-           request . body               =   payload . to_json 
61+            def  deep_compact ( payload ) 
62+              payload . dig ( :message ,   :android ,   :notification ) . try ( & :compact! ) 
63+              payload . dig ( :message ,   :android ) . try ( & :compact! ) 
64+              payload [ :message ] . compact! 
65+              payload 
66+           end 
6567
66-           rescue_and_reraise_network_errors  do 
67-             Net ::HTTP . start ( uri . host ,  uri . port ,  use_ssl : true ,  read_timeout : config [ :request_timeout ]  || DEFAULT_TIMEOUT )  do  |http |
68-               http . request ( request ) 
68+           # FCM requires data values to be strings. 
69+           def  stringify_data ( google_data ) 
70+             google_data . tap  do  |payload |
71+               payload [ :data ]  =  stringify ( payload [ :data ] )  if  payload [ :data ] 
6972            end 
7073          end 
71-         end 
7274
73-         def  rescue_and_reraise_network_errors 
74-           yield 
75-         rescue  Net ::ReadTimeout ,  Net ::OpenTimeout  =>  e 
76-           raise  ActionPushNative ::TimeoutError ,  e . message 
77-         rescue  Errno ::ECONNRESET ,  SocketError  =>  e 
78-           raise  ActionPushNative ::ConnectionError ,  e . message 
79-         rescue  OpenSSL ::SSL ::SSLError  =>  e 
80-           if  e . message . include? ( "SSL_connect" ) 
81-             raise  ActionPushNative ::ConnectionError ,  e . message 
82-           else 
83-             raise 
75+           def  stringify ( hash ) 
76+             hash . compact . transform_values ( &:to_s ) 
8477          end 
8578        end 
8679
@@ -92,28 +85,36 @@ def access_token
9285        end 
9386
9487        def  handle_error ( response ) 
95-           code  =  response . code 
88+           if  response . is_a? ( HTTPX ::ErrorResponse ) 
89+             handle_network_error ( response . error ) 
90+           else 
91+             handle_fcm_error ( response ) 
92+           end 
93+         end 
94+ 
95+         def  handle_fcm_error ( response ) 
96+           status  =  response . status 
9697          reason  =  \
9798            begin 
98-               JSON . parse ( response . body ) . dig ( "error" ,  "message" ) 
99+               JSON . parse ( response . body . to_s ) . dig ( "error" ,  "message" ) 
99100            rescue  JSON ::ParserError 
100-               response . body 
101+               response . body . to_s 
101102            end 
102103
103-           Rails . logger . error ( "FCM response error #{ code } #{ reason }  ) 
104+           Rails . logger . error ( "FCM response error #{ status } #{ reason }  ) 
104105
105106          case 
106107          when  reason  =~ /message is too big/i 
107108            raise  ActionPushNative ::PayloadTooLargeError ,  reason 
108-           when  code  == " 400" 
109+           when  status  == 400 
109110            raise  ActionPushNative ::BadRequestError ,  reason 
110-           when  code  == " 404" 
111+           when  status  == 404 
111112            raise  ActionPushNative ::TokenError ,  reason 
112-           when  code . in? ( [  " 401" ,   " 403" ] ) 
113+           when  status . in? ( [  401 ,   403  ] ) 
113114            raise  ActionPushNative ::ForbiddenError ,  reason 
114-           when  code  == " 429" 
115+           when  status  == 429 
115116            raise  ActionPushNative ::TooManyRequestsError ,  reason 
116-           when  code  == " 503" 
117+           when  status  == 503 
117118            raise  ActionPushNative ::ServiceUnavailableError ,  reason 
118119          else 
119120            raise  ActionPushNative ::InternalServerError ,  reason 
0 commit comments