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