Class: Parse::Client
- Inherits:
-
Object
- Object
- Parse::Client
- Includes:
- API::Aggregate, API::Analytics, API::Batch, API::CloudFunctions, API::Config, API::Files, API::Hooks, API::Objects, API::Push, API::Schema, API::Server, API::Sessions, API::Users
- Defined in:
- lib/parse/client.rb
Overview
This class is the core and low level API for the Parse SDK REST interface that is used by the other components. It can manage multiple sessions, which means you can have multiple client instances pointing to different Parse Applications at the same time. It handles sending raw requests as well as providing Request/Response objects for all API handlers. The connection engine is Faraday, which means it is open to add any additional middleware for features you'd like to implement.
Defined Under Namespace
Modules: Connectable Classes: DuplicateValueError, ResponseError
Constant Summary collapse
- USER_AGENT_HEADER =
The user agent header key.
"User-Agent".freeze
- USER_AGENT_VERSION =
The value for the User-Agent header.
"Parse-Stack v#{Parse::Stack::VERSION}".freeze
- DEFAULT_RETRIES =
The default retry count
2- RETRY_DELAY =
The wait time in seconds between retries
1.5- LIVE_QUERY_LOOPBACK_HOSTS =
Hosts considered "loopback" for the cleartext-ws:// guard in #configure_live_query. Mirrors LiveQuery::Client::LOOPBACK_HOSTS so the explicit-URL path and the derived-URL path agree on what counts as local.
%w[localhost 127.0.0.1 ::1 [::1] 0.0.0.0].freeze
Constants included from API::Server
API::Server::DEPRECATED_SERVER_VERSION_BELOW
Constants included from API::Hooks
Class Attribute Summary collapse
-
.clients ⇒ Object
readonly
Returns the value of attribute clients.
Instance Attribute Summary collapse
-
#api_key ⇒ String
readonly
The Parse API key to be sent in every API request.
-
#application_id ⇒ String
(also: #app_id)
readonly
The Parse application identifier to be sent in every API request.
-
#cache ⇒ Moneta::Transformer, Moneta::Expires
The underlying cache store for caching API requests.
-
#master_key ⇒ String
readonly
The Parse master key for this application, which when set, will be sent in every API request.
-
#retry_limit ⇒ Integer
If set, returns the current retry count for this instance.
-
#server_url ⇒ String
readonly
The Parse server url that will be receiving these API requests.
Attributes included from API::Server
Attributes included from API::Config
Class Method Summary collapse
-
.client(conn = :default) ⇒ Parse::Client
Returns or create a new Parse::Client connection for the given connection name.
-
.client?(conn = :default) ⇒ Boolean
True if a Parse::Client has been configured.
-
.setup(opts = {}) { ... } ⇒ Client
Setup the a new client with the appropriate Parse app keys, middleware and options.
Instance Method Summary collapse
-
#body_carries_atomic_op?(body) ⇒ Boolean
Whether a request body carries a Parse atomic operation, i.e.
-
#clear_cache! ⇒ Object
Clear the client cache.
-
#configure_live_query(opts) ⇒ Object
private
Configure LiveQuery with the given options.
-
#connected?(endpoint = nil) ⇒ Boolean
Connectivity probe.
-
#delete(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a DELETE request.
-
#get(uri, query = nil, headers = {}) ⇒ Parse::Response
Send a GET request.
-
#idempotent_retry?(method, body, headers = nil) ⇒ Boolean
Whether a request whose outcome is UNKNOWN (a 500/503 or a dropped connection) is safe to transparently re-send.
-
#initialize(opts = {}) ⇒ Client
constructor
Create a new client connected to the Parse Server REST API endpoint.
-
#post(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a POST request.
-
#put(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a PUT request.
-
#reachable? ⇒ Boolean
No-credentials liveness probe.
-
#request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {}) ⇒ Parse::Response
Send a REST API request to the server.
-
#send_request(req) ⇒ Parse::Response
Send a Request object.
-
#server_deduped_request?(headers) ⇒ Boolean
Whether this request is covered by Parse Server's server-side request-id deduplication, making a replay a no-op.
-
#url_prefix ⇒ String
The url prefix of the Parse Server url.
- #validate_live_query_url!(url, allow_insecure:) ⇒ Object private
- #warn_about_unknown_live_query_keys!(live_query_opts) ⇒ Object private
Methods included from API::Users
#create_user, #current_user, #delete_user, #fetch_user, #find_users, #login, #login_with_mfa, #logout, #request_password_reset, #set_service_auth_data, #signup, #update_user
Methods included from API::Sessions
Methods included from API::Server
#server_health, #server_info!, #server_version
Methods included from API::Schema
#create_schema, #schema, #schemas, #update_schema
Methods included from API::Push
Methods included from API::Objects
#create_object, #delete_object, #fetch_object, #find_objects, #update_object, #uri_path
Methods included from API::Hooks
#create_function, #create_trigger, #delete_function, #delete_trigger, #fetch_function, #fetch_trigger, #functions, #triggers, #update_function, #update_trigger
Methods included from API::Files
Methods included from API::Config
#config!, #config_entries, #update_config
Methods included from API::CloudFunctions
#call_function, #call_function_with_session, #trigger_job, #trigger_job_with_session
Methods included from API::Batch
Methods included from API::Aggregate
#aggregate_objects, #aggregate_pipeline, #aggregate_uri_path
Methods included from API::Analytics
Constructor Details
#initialize(opts = {}) ⇒ Client
Create a new client connected to the Parse Server REST API endpoint.
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 |
# File 'lib/parse/client.rb', line 491 def initialize(opts = {}) @server_url = opts[:server_url] || ENV["PARSE_SERVER_URL"] || Parse::Protocol::SERVER_URL @application_id = opts[:application_id] || opts[:app_id] || ENV["PARSE_SERVER_APPLICATION_ID"] || ENV["PARSE_APP_ID"] @api_key = opts[:api_key] || opts[:rest_api_key] || ENV["PARSE_SERVER_REST_API_KEY"] || ENV["PARSE_API_KEY"] @master_key = opts[:master_key] || ENV["PARSE_SERVER_MASTER_KEY"] || ENV["PARSE_MASTER_KEY"] @require_https = opts.fetch(:require_https, ENV["PARSE_REQUIRE_HTTPS"] == "true") @allow_faraday_proxy = opts.fetch(:allow_faraday_proxy, false) # Security check for HTTP usage (except localhost/127.0.0.1 for development) if @server_url&.start_with?("http://") && !@server_url.match?(%r{^http://(localhost|127\.0\.0\.1)(:|/)}) if @require_https raise ArgumentError, "[Parse::Client] HTTPS required but server URL uses HTTP: #{@server_url}. " \ "Set require_https: false or use an HTTPS URL." else warn "[Parse::Client] SECURITY WARNING: Using HTTP instead of HTTPS for Parse server. " \ "This exposes credentials and data to network interception. " \ "Use HTTPS in production: #{@server_url}" end end # Determine the HTTP adapter to use # Priority: explicit :adapter > :connection_pooling setting > default (pooling enabled) # Falls back to default adapter if net_http_persistent is not available if opts[:adapter] # User explicitly specified an adapter, use it directly adapter = opts[:adapter] = {} elsif opts[:connection_pooling] == false # User explicitly disabled connection pooling adapter = Faraday.default_adapter = {} elsif opts[:connection_pooling].is_a?(Hash) # User provided connection pooling with custom options if NET_HTTP_PERSISTENT_AVAILABLE adapter = :net_http_persistent = opts[:connection_pooling] else adapter = Faraday.default_adapter = {} end else # Default: use persistent connections for better performance (if available) if NET_HTTP_PERSISTENT_AVAILABLE adapter = :net_http_persistent = {} else adapter = Faraday.default_adapter = {} end end opts[:expires] ||= 3 if @server_url.nil? || @application_id.nil? || (@api_key.nil? && @master_key.nil?) raise Parse::Error::ConnectionError, "Please call Parse.setup(server_url:, application_id:, api_key:) to setup a client" end @server_url += "/" unless @server_url.ends_with?("/") # Resolve timeouts. Defaults guard the calling thread against an # unresponsive Parse Server (slowloris, hung dyno) which would # otherwise tie up Puma/Sidekiq workers indefinitely. open_timeout = opts.fetch(:open_timeout, (ENV["PARSE_OPEN_TIMEOUT"] || 5).to_i) read_timeout = opts.fetch(:timeout, (ENV["PARSE_TIMEOUT"] || 30).to_i) #Configure Faraday opts[:faraday] ||= {} # Guard against silent TLS downgrade or attacker-controlled proxy via # opts[:faraday]. The require_https check earlier only inspects the URL # scheme; without this guard a caller passing # faraday: { ssl: { verify: false }, proxy: "http://attacker" } # would neuter TLS verification on an HTTPS connection. validate_faraday_opts!(opts[:faraday]) opts[:faraday].merge!(:url => @server_url) @conn = Faraday.new(opts[:faraday]) do |conn| # Apply timeouts before any user-supplied middleware sees a request. conn..timeout = read_timeout if read_timeout > 0 conn..open_timeout = open_timeout if open_timeout > 0 #conn.request :json # Configure logging if enabled if opts[:logging].present? # Configure the new structured logging middleware Parse::Middleware::Logging.enabled = true Parse::Middleware::Logging.logger = opts[:logger] if opts[:logger] case opts[:logging] when :debug Parse::Middleware::Logging.log_level = :debug Parse::Middleware::BodyBuilder.logging = true when :warn Parse::Middleware::Logging.log_level = :warn else Parse::Middleware::Logging.log_level = :info end end # This middleware handles sending the proper authentication headers to Parse # on each request. # this is the required authentication middleware. Should be the first thing # so that other middlewares have access to the env that is being set by # this middleware. First added is first to brocess. conn.use Parse::Middleware::Authentication, application_id: @application_id, master_key: @master_key, api_key: @api_key # Request/response logging middleware (configured via Parse.logging_enabled) conn.use Parse::Middleware::Logging # Performance profiling middleware (configured via Parse.profiling_enabled) conn.use Parse::Middleware::Profiling # This middleware turns the result from Parse into a Parse::Response object # and making sure request that are going out, follow the proper MIME format. # We place it after the Authentication middleware in case we need to use then # authentication information when building request and responses. conn.use Parse::Middleware::BodyBuilder if opts[:cache].present? if opts[:expires].to_i <= 0 warn "[Parse::Client] Cache store provided but :expires is not set or is 0. " \ "Caching will be disabled. Set :expires to enable caching (e.g., expires: 10)." else # advanced: provide a REDIS url, we'll configure a Moneta Redis store. if opts[:cache].is_a?(String) && opts[:cache].starts_with?("redis://") begin opts[:cache] = Moneta.new(:Redis, url: opts[:cache]) rescue LoadError puts "[Parse::Middleware::Caching] Did you forget to load the redis gem (Gemfile)?" raise end end unless [:key?, :[], :delete, :store].all? { |method| opts[:cache].respond_to?(method) } raise ArgumentError, "Parse::Client option :cache needs to be a type of Moneta store" end # If the caller passed a `Parse::Cache::Redis` wrapper, let its # built-in namespace flow through automatically. An explicit # `cache_namespace:` still wins so callers can override. if defined?(Parse::Cache::Redis) && opts[:cache].is_a?(Parse::Cache::Redis) opts[:cache_namespace] ||= opts[:cache].namespace end self.cache = opts[:cache] conn.use Parse::Middleware::Caching, self.cache, { expires: opts[:expires].to_i, # Optional `cache_namespace:` prefixes every key so two Parse # apps sharing one Redis don't collide on `mk:/classes/Song/abc`. # Explicit only — we do NOT auto-derive from app_id to keep # existing single-app deployments backward-compatible. namespace: opts[:cache_namespace], } # Inform about opt-in cache behavior unless Parse.default_query_cache warn "[Parse::Client] Caching middleware enabled (expires: #{opts[:expires]}s). " \ "Queries do NOT use cache by default. Use `cache: true` on queries to opt-in, " \ "or set `Parse.default_query_cache = true` for opt-out behavior." end end end yield(conn) if block_given? # Configure the adapter with optional settings # For net_http_persistent: # - pool_size must be passed as an adapter argument (constructor param, no setter) # - idle_timeout and keep_alive have setters and are configured in the block if .any? # Extract constructor arguments for the adapter adapter_args = {} adapter_args[:pool_size] = [:pool_size] if [:pool_size] conn.adapter adapter, **adapter_args do |http| http.idle_timeout = [:idle_timeout] if [:idle_timeout] http.keep_alive = [:keep_alive] if [:keep_alive] end else conn.adapter adapter end end # Faraday's constructor may still synthesise a ProxyOptions from # HTTPS_PROXY/HTTP_PROXY env vars regardless of the `proxy: nil` # we pass in opts. Clear the proxy on the connection itself to be # sure no env-derived MITM survives. @conn.proxy = nil if !@allow_faraday_proxy && @conn.respond_to?(:proxy=) Parse::Client.clients[:default] ||= self # Configure LiveQuery if URL provided configure_live_query(opts) end |
Class Attribute Details
.clients ⇒ Object (readonly)
Returns the value of attribute clients.
343 344 345 |
# File 'lib/parse/client.rb', line 343 def clients @clients end |
Instance Attribute Details
#api_key ⇒ String (readonly)
The Parse API key to be sent in every API request.
331 |
# File 'lib/parse/client.rb', line 331 attr_accessor :cache |
#application_id ⇒ String (readonly) Also known as: app_id
The Parse application identifier to be sent in every API request.
331 |
# File 'lib/parse/client.rb', line 331 attr_accessor :cache |
#cache ⇒ Moneta::Transformer, Moneta::Expires
The underlying cache store for caching API requests.
331 332 333 |
# File 'lib/parse/client.rb', line 331 def cache @cache end |
#master_key ⇒ String (readonly)
The Parse master key for this application, which when set, will be sent in every API request. (There is a way to prevent this on a per request basis.)
331 |
# File 'lib/parse/client.rb', line 331 attr_accessor :cache |
#retry_limit ⇒ Integer
If set, returns the current retry count for this instance. Otherwise, returns DEFAULT_RETRIES. Set to 0 to disable retry mechanism.
331 |
# File 'lib/parse/client.rb', line 331 attr_accessor :cache |
#server_url ⇒ String (readonly)
The Parse server url that will be receiving these API requests. By default this will be Protocol::SERVER_URL.
331 |
# File 'lib/parse/client.rb', line 331 attr_accessor :cache |
Class Method Details
.client(conn = :default) ⇒ Parse::Client
Returns or create a new Parse::Client connection for the given connection name.
355 356 357 |
# File 'lib/parse/client.rb', line 355 def client(conn = :default) @clients[conn] ||= self.new end |
.client?(conn = :default) ⇒ Boolean
Returns true if a Parse::Client has been configured.
347 348 349 |
# File 'lib/parse/client.rb', line 347 def client?(conn = :default) @clients[conn].present? end |
.setup(opts = {}) { ... } ⇒ Client
Setup the a new client with the appropriate Parse app keys, middleware and options.
374 375 376 |
# File 'lib/parse/client.rb', line 374 def setup(opts = {}, &block) @clients[:default] = self.new(opts, &block) end |
Instance Method Details
#body_carries_atomic_op?(body) ⇒ Boolean
Whether a request body carries a Parse atomic operation, i.e. any field
whose value is a Hash with an __op key (Increment, Add, AddUnique,
Remove, AddRelation, RemoveRelation, Delete). Such ops are not idempotent
and must not be replayed on an ambiguous failure. Assumes the body is a
Ruby Hash, which the SDK's normal save/update path always provides; a
pre-serialized String body is treated as op-free (and therefore
retryable), so callers handing request a raw JSON string for a
PUT-with-op would bypass this guard.
1201 1202 1203 1204 |
# File 'lib/parse/client.rb', line 1201 def body_carries_atomic_op?(body) return false unless body.is_a?(Hash) body.any? { |_k, v| v.is_a?(Hash) && (v.key?("__op") || v.key?(:__op)) } end |
#clear_cache! ⇒ Object
Clear the client cache
828 829 830 |
# File 'lib/parse/client.rb', line 828 def clear_cache! self.cache.clear if self.cache.present? end |
#configure_live_query(opts) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Configure LiveQuery with the given options
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 |
# File 'lib/parse/client.rb', line 736 def configure_live_query(opts) live_query_url = opts[:live_query_url] || ENV["PARSE_LIVE_QUERY_URL"] return unless live_query_url || opts[:live_query] require_relative "live_query" live_query_opts = opts[:live_query].is_a?(Hash) ? opts[:live_query] : {} resolved_url = live_query_url || live_query_opts[:url] # Refuse explicit `ws://` against a non-loopback host unless # `allow_insecure: true` is also passed in `live_query:`. The # downstream `derive_websocket_url` path already enforces this for # URLs derived from a Parse Server `http://` URL, but an explicit # `live_query: { url: "ws://prod-host" }` or # `live_query_url: "ws://prod-host"` bypassed it — the master key # and any session token would ride the connect frame in cleartext. validate_live_query_url!(resolved_url, allow_insecure: live_query_opts[:allow_insecure]) # Warn (don't raise) on `live_query: { ... }` keys that are not # `Parse::LiveQuery::Configuration` setters. The block form would # otherwise silently swallow typos like # `live_query: { ssl_min_versoin: :TLSv1_3 }` and leave TLS at the # default, losing the operator's intent. The pre-fix kwargs form # raised `ArgumentError` here; this restores the surface without # making it a hard failure for unknown-but-harmless keys. warn_about_unknown_live_query_keys!(live_query_opts) Parse::LiveQuery.configure do |config| config.application_id = @application_id if @application_id config.client_key = @api_key if @api_key config.master_key = @master_key if @master_key # Apply hash-form options first so the resolved URL (which honors # top-level `live_query_url:` over `live_query: { url: }`) wins. # Without this, the loop would re-write `config.url` from the # hash and silently invert the documented precedence. live_query_opts.each do |key, value| next if key == :url setter = "#{key}=" config.public_send(setter, value) if config.respond_to?(setter) end config.url = resolved_url if resolved_url end end |
#connected?(endpoint = nil) ⇒ Boolean
Connectivity probe. By default hits the Parse Server health endpoint — the same target as #reachable? — so it returns +true+ whenever the server is up, regardless of CLP configuration.
Why not a _User find? A limit-0 find against +_User+ exercises the auth
stack, but locking +_User+ finds to the master key via a Class-Level
Permission is standard production hardening — on such a server the probe
gets a permission error and (wrongly) reports "not connected" for a
perfectly healthy, correctly-configured deployment. The default therefore
avoids any data class.
To ALSO validate credentials, pass endpoint: a path to a class the
configured key is allowed to read (e.g. "classes/_User" on a default
server, or one of your own readable classes). The probe runs limit: 0
so it never pulls rows, and routes through the auth middleware, so a wrong
application_id / REST key surfaces as an Error::AuthenticationError
and is converted to +false+. Any connection, timeout, or API error returns
+false+ rather than raising; genuine programming errors (e.g.
+NoMethodError+) still propagate.
868 869 870 871 872 873 874 |
# File 'lib/parse/client.rb', line 868 def connected?(endpoint = nil) path = endpoint || Parse::API::Server::SERVER_HEALTH_PATH response = request(:get, path, query: { limit: 0 }, opts: { cache: false }) response.success? rescue Parse::Error, Faraday::Error false end |
#delete(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a DELETE request.
1238 1239 1240 |
# File 'lib/parse/client.rb', line 1238 def delete(uri, body = nil, headers = {}) request :delete, uri, body: body, headers: headers end |
#get(uri, query = nil, headers = {}) ⇒ Parse::Response
Send a GET request.
1211 1212 1213 |
# File 'lib/parse/client.rb', line 1211 def get(uri, query = nil, headers = {}) request :get, uri, query: query, headers: headers end |
#idempotent_retry?(method, body, headers = nil) ⇒ Boolean
Whether a request whose outcome is UNKNOWN (a 500/503 or a dropped connection) is safe to transparently re-send.
Server-dedup fast path: when the operator has asserted Parse Server
idempotency is configured (Request.assume_server_idempotency)
AND this request carries a stable X-Parse-Request-Id header, the
server deduplicates a replay, so even a POST or an atomic-op write is
safe to retry — the write applies at most once. The replay is NOT a
transparent success, though: Parse Server rejects the duplicate with
error 159, surfaced as a raised Error::DuplicateRequestError
the caller must rescue (the original write already landed). The SDK sends
the same request id on every retry (the header is set once and preserved
across the retry), which is what makes the server-side dedup match.
Otherwise the conservative method/body heuristic applies: GET and DELETE
are idempotent; a full-object PUT update is idempotent ONLY when it
carries no atomic __op mutation (Increment/Add/AddUnique/Remove/Relation
would double-apply on replay); POST (object create / batch) is never
auto-retried. 429 throttles are handled at the call site, since the
server provably discarded the request and those re-send regardless.
1167 1168 1169 1170 1171 1172 1173 1174 |
# File 'lib/parse/client.rb', line 1167 def idempotent_retry?(method, body, headers = nil) return true if server_deduped_request?(headers) case method when :get, :delete then true when :put then !body_carries_atomic_op?(body) else false end end |
#post(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a POST request.
1220 1221 1222 |
# File 'lib/parse/client.rb', line 1220 def post(uri, body = nil, headers = {}) request :post, uri, body: body, headers: headers end |
#put(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a PUT request.
1229 1230 1231 |
# File 'lib/parse/client.rb', line 1229 def put(uri, body = nil, headers = {}) request :put, uri, body: body, headers: headers end |
#reachable? ⇒ Boolean
No-credentials liveness probe. Hits the Parse Server health endpoint and returns +true+ when the server responds with status "ok". No application credentials are required, so this passes even when the configured application_id or REST key is wrong. Use #connected? to also validate credentials.
838 839 840 841 842 843 |
# File 'lib/parse/client.rb', line 838 def reachable? response = request(:get, Parse::API::Server::SERVER_HEALTH_PATH, opts: { cache: false }) response.success? rescue Parse::Error, Faraday::Error false end |
#request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {}) ⇒ Parse::Response
Send a REST API request to the server. This is the low-level API used for all requests to the Parse server with the provided options. Every request sent to Parse through the client goes through the configured set of middleware that can be modified by applying different headers or specific options. This method supports retrying requests a few times when a ServiceUnavailableError is raised.
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 |
# File 'lib/parse/client.rb', line 926 def request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {}) # Pre-declare locals referenced inside rescue blocks so CodeQL's # uninitialized-variable analysis is satisfied even if an exception # raises before the natural assignment site. response = nil _retry_count = nil _retry_delay = nil _request = nil # Kwarg-absorption guard. The `**opts` splat in API helper methods # (lib/parse/api/*.rb) absorbs a caller-passed `opts: { ... }` # keyword as a key named `:opts` rather than as the request options # hash itself. The auth context (session_token, use_master_key) # buried under :opts then never reaches the request — the call # silently goes out anonymous (or master, if one is configured), # which is a permission-downgrade footgun. Fail loudly here so the # bug surfaces in dev/test instead of in production. if opts.is_a?(Hash) && opts[:opts].is_a?(Hash) raise ArgumentError, "Parse::Client#request received nested `opts: { opts: { ... } }` — " \ "pass session_token: / use_master_key: directly as keywords, " \ "not wrapped in an `opts:` hash. " \ "Bad: Parse.client.create_object('X', body, opts: { session_token: t }) " \ "Good: Parse.client.create_object('X', body, session_token: t, use_master_key: false)" end # Retry budget. Initialized ONCE here, ABOVE the `begin` below, so the # `retry` keyword in the rescue clauses (which re-runs only the begin # block, not the whole method) preserves the countdown across attempts. # If this initialization ran inside the begin it would reset on every # attempt, turning a transient 500/503/429 into an infinite retry loop. _retry_count ||= self.retry_limit if opts[:retry] == false _retry_count = 0 elsif opts[:retry].to_i > 0 _retry_count = opts[:retry] end # The effective starting budget, captured ONCE after the opts override # (and, like `_retry_count`, above the `begin` so `retry` doesn't reset # it). The backoff multiplier is `(_retry_max - _retry_count)`, which # grows 1, 2, 3, … as attempts are consumed. Deriving it from # `self.retry_limit` instead would go to zero or negative whenever a # caller passes `opts: { retry: N }` with N above the instance default, # silently disabling the backoff (every retry firing at zero delay). _retry_max ||= _retry_count begin headers ||= {} # if the first argument is a Parse::Request object, then construct it _request = nil if method.is_a?(Request) _request = method method = _request.method uri ||= _request.path query ||= _request.query body ||= _request.body headers.merge! _request.headers else _request = Parse::Request.new(method, uri, body: body, headers: headers, opts: opts) end # http method method = method.downcase.to_sym # set the User-Agent headers[USER_AGENT_HEADER] = USER_AGENT_VERSION if opts[:cache] == false headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache" elsif opts[:cache] == :write_only # Write-only mode: skip reading from cache, but still write to cache # Useful for fetch!/reload! which want fresh data but should update cache headers[Parse::Middleware::Caching::CACHE_WRITE_ONLY] = "true" elsif opts[:cache].is_a?(Numeric) # specify the cache duration of this request headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_s end # Resolve the auth context in three layers: # 1. explicit per-call `use_master_key:` and `session_token:` # 2. ambient session set by `Parse.with_session { ... }` (fiber-local) # 3. process-wide `Parse.client_mode` flag — when true, master key is # never sent unless the caller explicitly passed `use_master_key: true` explicit_master = opts.key?(:use_master_key) if opts[:use_master_key] == false headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true" elsif Parse.client_mode && opts[:use_master_key] != true # client mode defaults master key OFF unless explicitly opted in headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true" end token = opts[:session_token] # When no explicit token was passed AND the caller didn't ask to send # the master key, fall through to the fiber-local ambient set by # `Parse.with_session`. Explicit `use_master_key: true` is treated as # a deliberate admin call and skips the ambient — otherwise an # `admin.do_thing(use_master_key: true)` nested inside a # `with_session(user)` block would silently downgrade. if token.nil? && !(explicit_master && opts[:use_master_key] == true) ambient = Parse.current_session_token token = ambient if ambient.is_a?(String) && !ambient.empty? end if token.present? token = token.session_token if token.respond_to?(:session_token) headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true" headers[Parse::Protocol::SESSION_TOKEN] = token end #if it is a :get request, then use query params, otherwise body. params = (method == :get ? query : body) || {} # if the path does not start with the '/1/' prefix, then add it to be nice. # actually send the request and return the body response_env = @conn.send(method, uri, params, headers) response = response_env.body response.request = _request case response.http_status when 401, 403 Parse::Client._safe_warn("AuthenticationError", response) raise Parse::Error::AuthenticationError, response when 400, 408 if response.code == Parse::Response::ERROR_TIMEOUT || response.code == 143 #"net/http: timeout awaiting response headers" Parse::Client._safe_warn("TimeoutError", response) raise Parse::Error::TimeoutError, response end when 404 unless response.object_not_found? Parse::Client._safe_warn("ConnectionError", response) raise Parse::Error::ConnectionError, response end when 405, 406 Parse::Client._safe_warn("ProtocolError", response) raise Parse::Error::ProtocolError, response when 429 # Request over the throttle limit Parse::Client._safe_warn("RequestLimitExceededError", response) raise Parse::Error::RequestLimitExceededError, response when 500, 503 Parse::Client._safe_warn("ServiceUnavailableError", response) raise Parse::Error::ServiceUnavailableError, response end if response.error? if response.code <= Parse::Response::ERROR_SERVICE_UNAVAILABLE Parse::Client._safe_warn("ServiceUnavailableError", response) raise Parse::Error::ServiceUnavailableError, response elsif response.code <= 100 Parse::Client._safe_warn("ServerError", response) raise Parse::Error::ServerError, response elsif response.code == Parse::Response::ERROR_EXCEEDED_BURST_LIMIT Parse::Client._safe_warn("RequestLimitExceededError", response) raise Parse::Error::RequestLimitExceededError, response elsif response.code == 209 # Error 209: invalid session token Parse::Client._safe_warn("InvalidSessionTokenError", response) raise Parse::Error::InvalidSessionTokenError, response elsif response.code == Parse::Response::ERROR_DUPLICATE_REQUEST # 159 # Request-id idempotency rejected a duplicate — the original write # already applied (NOT a second time). Surface a typed, catchable # signal rather than a generic error; this is what a transparently- # retried write that landed-but-lost-its-response sees on the replay. Parse::Client._safe_warn("DuplicateRequestError", response) raise Parse::Error::DuplicateRequestError, response end end response rescue Parse::Error::RequestLimitExceededError, Parse::Error::ServiceUnavailableError => e # 429 (RequestLimitExceeded): the server threw the request away, so # re-sending is safe for any method. 500/503 (ServiceUnavailable) is # ambiguous — a write may have applied before the error — so only # re-send when the request is idempotent (see #idempotent_retry?). retryable = e.is_a?(Parse::Error::RequestLimitExceededError) || idempotent_retry?(method, body, headers) if _retry_count > 0 && retryable warn "[Parse:Retry] Retries remaining #{_retry_count} : #{response.request}" _retry_count -= 1 # Use Retry-After header if available, otherwise use linear backoff retry_after = response.retry_after if response.respond_to?(:retry_after) if retry_after && retry_after > 0 _retry_delay = retry_after warn "[Parse:Retry] Using Retry-After header: #{_retry_delay}s" else # Linear backoff (RETRY_DELAY × attempt number) with +/-25% jitter. # Never zero — # zero-wait retries amplify DoS against upstream and stampede on 429. backoff_delay = RETRY_DELAY * (_retry_max - _retry_count) _retry_delay = backoff_delay * (0.75 + rand * 0.5) end sleep _retry_delay if _retry_delay > 0 retry end raise rescue Faraday::ClientError, Faraday::TimeoutError, Net::OpenTimeout => e # Request timed out mid-flight: the outcome is unknown (the server may # have received and applied the write but never answered), so only # re-send idempotent requests to avoid double-applying. # # Faraday 2.x raises `Faraday::TimeoutError` for a read timeout # (`Timeout::Error` / `Errno::ETIMEDOUT`); it subclasses `Faraday::Error`, # not `ClientError`, so it must be listed explicitly to be caught. We # deliberately do NOT catch `Faraday::ConnectionFailed` (connection # refused/reset, plus the wrapped connect-timeout): refused is a # non-transient "server down / misconfigured" failure, and auto-retrying # it only adds backoff latency before the inevitable error. Broadening to # reset connections safely (retry reset, fail fast on refused) is tracked # as a follow-up. if _retry_count > 0 && idempotent_retry?(method, body, headers) warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}" _retry_count -= 1 backoff_delay = RETRY_DELAY * (_retry_max - _retry_count) _retry_delay = backoff_delay * (0.75 + rand * 0.5) sleep _retry_delay if _retry_delay > 0 retry end raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.}" end end |
#send_request(req) ⇒ Parse::Response
Send a Request object.
1246 1247 1248 1249 |
# File 'lib/parse/client.rb', line 1246 def send_request(req) #Parse::Request object raise ArgumentError, "Object not of Parse::Request type." unless req.is_a?(Parse::Request) request req.method, req.path, req.body, req.headers end |
#server_deduped_request?(headers) ⇒ Boolean
Whether this request is covered by Parse Server's server-side request-id deduplication, making a replay a no-op. True only when the operator has opted in via Request.assume_server_idempotency AND the request actually carries a non-blank request-id header (writes to inherently non-idempotent paths — sessions, logout, functions, push, jobs — never get a request id, so they correctly fail this check).
1184 1185 1186 1187 1188 1189 |
# File 'lib/parse/client.rb', line 1184 def server_deduped_request?(headers) return false unless Parse::Request.assume_server_idempotency return false unless headers.is_a?(Hash) rid = headers[Parse::Request.request_id_header] rid.is_a?(String) && !rid.strip.empty? end |
#url_prefix ⇒ String
Returns the url prefix of the Parse Server url.
823 824 825 |
# File 'lib/parse/client.rb', line 823 def url_prefix @conn.url_prefix end |
#validate_live_query_url!(url, allow_insecure:) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
784 785 786 787 788 789 790 791 792 793 794 795 796 797 |
# File 'lib/parse/client.rb', line 784 def validate_live_query_url!(url, allow_insecure:) return unless url.is_a?(String) && url.start_with?("ws://") host = URI.parse(url).host.to_s rescue "" return if LIVE_QUERY_LOOPBACK_HOSTS.include?(host) return if allow_insecure raise ArgumentError, "[Parse::Client] Refusing explicit insecure LiveQuery URL #{url.inspect}. " \ "The connect frame carries the master key and any session token in " \ "plaintext on this socket. Use wss:// for routable hosts, or pass " \ "`live_query: { allow_insecure: true }` to opt into cleartext for " \ "local development on a non-loopback address." end |
#warn_about_unknown_live_query_keys!(live_query_opts) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
800 801 802 803 804 805 806 807 808 809 810 811 812 |
# File 'lib/parse/client.rb', line 800 def warn_about_unknown_live_query_keys!(live_query_opts) return unless live_query_opts.is_a?(Hash) && live_query_opts.any? probe = Parse::LiveQuery::Configuration.new unknown = live_query_opts.keys.reject { |k| probe.respond_to?("#{k}=") } return if unknown.empty? warn "[Parse::Client] Ignoring unknown live_query option(s): " \ "#{unknown.inspect}. Valid keys are Parse::LiveQuery::Configuration " \ "setters (url, application_id, client_key, master_key, ping_interval, " \ "pong_timeout, allow_insecure, ssl_min_version, ssl_max_version, " \ "logging_enabled, log_level, ...). Check for typos." end |