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
23+ def httpx_session
24+ self . class . httpx_sessions ||= { }
25+ self . class . httpx_sessions [ config ] ||= build_httpx_session
26+ end
27+
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
32+
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" )
39+ end
40+
2241 def payload_from ( notification )
2342 deep_compact ( {
2443 message : {
@@ -56,34 +75,6 @@ def stringify(hash)
5675 hash . compact . transform_values ( &:to_s )
5776 end
5877
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
65-
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 )
69- end
70- end
71- end
72-
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
84- end
85- end
86-
8778 def access_token
8879 authorizer = Google ::Auth ::ServiceAccountCredentials . make_creds \
8980 json_key_io : StringIO . new ( config . fetch ( :encryption_key ) ) ,
@@ -92,28 +83,36 @@ def access_token
9283 end
9384
9485 def handle_error ( response )
95- code = response . code
86+ if response . is_a? ( HTTPX ::ErrorResponse )
87+ handle_network_error ( response . error )
88+ else
89+ handle_fcm_error ( response )
90+ end
91+ end
92+
93+ def handle_fcm_error ( response )
94+ status = response . status
9695 reason = \
9796 begin
98- JSON . parse ( response . body ) . dig ( "error" , "message" )
97+ JSON . parse ( response . body . to_s ) . dig ( "error" , "message" )
9998 rescue JSON ::ParserError
100- response . body
99+ response . body . to_s
101100 end
102101
103- Rails . logger . error ( "FCM response error #{ code } : #{ reason } " )
102+ Rails . logger . error ( "FCM response error #{ status } : #{ reason } " )
104103
105104 case
106105 when reason =~ /message is too big/i
107106 raise ActionPushNative ::PayloadTooLargeError , reason
108- when code == " 400"
107+ when status == 400
109108 raise ActionPushNative ::BadRequestError , reason
110- when code == " 404"
109+ when status == 404
111110 raise ActionPushNative ::TokenError , reason
112- when code . in? ( [ " 401" , " 403" ] )
111+ when status . in? ( [ 401 , 403 ] )
113112 raise ActionPushNative ::ForbiddenError , reason
114- when code == " 429"
113+ when status == 429
115114 raise ActionPushNative ::TooManyRequestsError , reason
116- when code == " 503"
115+ when status == 503
117116 raise ActionPushNative ::ServiceUnavailableError , reason
118117 else
119118 raise ActionPushNative ::InternalServerError , reason
0 commit comments