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
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
-
#clear_cache! ⇒ Object
Clear the client cache.
-
#configure_live_query(opts) ⇒ Object
private
Configure LiveQuery with the given options.
-
#delete(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a DELETE request.
-
#get(uri, query = nil, headers = {}) ⇒ Parse::Response
Send a GET request.
-
#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.
-
#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.
-
#url_prefix ⇒ String
The url prefix of the Parse Server url.
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.
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 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 |
# File 'lib/parse/client.rb', line 433 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 self.cache = opts[:cache] conn.use Parse::Middleware::Caching, self.cache, { expires: opts[:expires].to_i } # 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.
295 296 297 |
# File 'lib/parse/client.rb', line 295 def clients @clients end |
Instance Attribute Details
#api_key ⇒ String (readonly)
The Parse API key to be sent in every API request.
283 |
# File 'lib/parse/client.rb', line 283 attr_accessor :cache |
#application_id ⇒ String (readonly) Also known as: app_id
The Parse application identifier to be sent in every API request.
283 |
# File 'lib/parse/client.rb', line 283 attr_accessor :cache |
#cache ⇒ Moneta::Transformer, Moneta::Expires
The underlying cache store for caching API requests.
283 284 285 |
# File 'lib/parse/client.rb', line 283 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.)
283 |
# File 'lib/parse/client.rb', line 283 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.
283 |
# File 'lib/parse/client.rb', line 283 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.
283 |
# File 'lib/parse/client.rb', line 283 attr_accessor :cache |
Class Method Details
.client(conn = :default) ⇒ Parse::Client
Returns or create a new Parse::Client connection for the given connection name.
307 308 309 |
# File 'lib/parse/client.rb', line 307 def client(conn = :default) @clients[conn] ||= self.new end |
.client?(conn = :default) ⇒ Boolean
Returns true if a Parse::Client has been configured.
299 300 301 |
# File 'lib/parse/client.rb', line 299 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.
326 327 328 |
# File 'lib/parse/client.rb', line 326 def setup(opts = {}, &block) @clients[:default] = self.new(opts, &block) end |
Instance Method Details
#clear_cache! ⇒ Object
Clear the client cache
689 690 691 |
# File 'lib/parse/client.rb', line 689 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
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 |
# File 'lib/parse/client.rb', line 657 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] : {} Parse::LiveQuery.configure( url: live_query_url || live_query_opts[:url], application_id: @application_id, client_key: @api_key, master_key: @master_key, **live_query_opts, ) end |
#delete(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a DELETE request.
903 904 905 |
# File 'lib/parse/client.rb', line 903 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.
876 877 878 |
# File 'lib/parse/client.rb', line 876 def get(uri, query = nil, headers = {}) request :get, uri, query: query, headers: headers end |
#post(uri, body = nil, headers = {}) ⇒ Parse::Response
Send a POST request.
885 886 887 |
# File 'lib/parse/client.rb', line 885 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.
894 895 896 |
# File 'lib/parse/client.rb', line 894 def put(uri, body = nil, headers = {}) request :put, uri, body: body, headers: headers 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.
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 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 |
# File 'lib/parse/client.rb', line 740 def request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {}) _retry_count ||= self.retry_limit if opts[:retry] == false _retry_count = 0 elsif opts[:retry].to_i > 0 _retry_count = opts[:retry] end 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 if opts[:use_master_key] == false headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true" end token = opts[:session_token] 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 end end response rescue Parse::Error::RequestLimitExceededError, Parse::Error::ServiceUnavailableError => e if _retry_count > 0 warn "[Parse:Retry] Retries remaining #{_retry_count} : #{response.request}" _retry_count -= 1 # Use Retry-After header if available, otherwise use exponential 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 # Deterministic exponential backoff with +/-25% jitter. Never zero — # zero-wait retries amplify DoS against upstream and stampede on 429. backoff_delay = RETRY_DELAY * (self.retry_limit - _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, Net::OpenTimeout => e if _retry_count > 0 warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}" _retry_count -= 1 backoff_delay = RETRY_DELAY * (self.retry_limit - _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 |
#send_request(req) ⇒ Parse::Response
Send a Request object.
911 912 913 914 |
# File 'lib/parse/client.rb', line 911 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 |
#url_prefix ⇒ String
Returns the url prefix of the Parse Server url.
684 685 686 |
# File 'lib/parse/client.rb', line 684 def url_prefix @conn.url_prefix end |