12/08/2018, 13:56

HTTP Calls in Ruby

Mở đầu Một API không tồn tại một mình. Luôn có hai bên liên quan: Đó là Client và Server. Trong Rails, các ứng dụng thường đóng vai trò là server, và chúng ta có nhiều cách để khắc phục sự cố các vấn đề phát sinh. Chúng tôi có thể đọc log để xem những request đến (path, params, ...), những ...

Mở đầu Một API không tồn tại một mình. Luôn có hai bên liên quan: Đó là Client và Server. Trong Rails, các ứng dụng thường đóng vai trò là server, và chúng ta có nhiều cách để khắc phục sự cố các vấn đề phát sinh. Chúng tôi có thể đọc log để xem những request đến (path, params, ...), những respond phản hồi, ... nhưng với việc backend ngày càng ngày càng phát triển theo kiến trúc microservices, các ứng dụng Rails đang thường xuyên hoạt động thêm cả bên client. Client cần thực hiện các yêu cầu HTTP và phân tích phản ứng JSON. Cụ thể hơn, client cần tạo các URL chính xác, tiêu đề HTTP để xác thực, phân trang, định dạng respond. Rất may là hầu hết các API phổ biến, chẳng hạn như Twitter, Slack, hay Stripe, đều có thư viện để xây dựng HTTP request và phân tích kết quả trả về. Nhưng khi phải kết nối với các dịch vụ nội bộ hoặc các API ít được biết đến, mà ko có API được xây dựng sẵn, chúng ta sẽ phải tự xây dựng. Phần còn lại của bài viết này sẽ khám phá một số các tùy chọn khác nhau có sẵn trong Ruby để đối phó với HTTP và một số kỹ thuật để tổ chức lại code và xử lý sự cố khi xảy ra sai sót.

Những HTTP client có sẵn trong Ruby Có một vài thư viện HTTP có sẵn trong Ruby sử dụng cho HTTP request, và chúng ta sẽ không thể chạm vào chúng. Phổ biến là:

  • Net::HTTP : Một phần của thư viện chuẩn Ruby. Nó có thể thực hiện tất cả mọi thứ bạn cần nó, và đó có thể là một lý do tại sao rất nhiều thư viện HTTP bên thứ ba tồn tại. Tuy nhiên nó không đơn giản để làm việc và mọi người thường có xu hướng sẽ sử dụng lựa chọn khác.
  • Curb: Gem này cung cấp các ràng buộc đối với các chương trình libcurl
  • HTTParty: Một thư viện rất phổ biến (22 triệu lượt tải về) mà bao gồm Net::HTTP, cung cấp một API dễ dàng hơn để làm việc với.
  • HTTP: Không phổ biến như các thư viện khác, nó là một yêu thích của tôi gần đây khi kết nối với API. Nó cung cấp một giao diện thể kết nối để xây dựng các yêu cầu HTTP và cung cấp hỗ trợ cho tất cả các chức năng thông thường mà bạn mong đợi từ một thư viện HTTP.
  • Excon: Một thư viện rất phổ biến (25 triệu lượt tải về) viết bằng Ruby thuần. Nó có một API sạch sẽ và rất dễ dàng để sử dụng.

Tôi sẽ không trình bày ví dụ làm thế nào để sử dụng mỗi một trong những thư viện; bạn sẽ có thể dễ dàng tìm thấy nó trong những đơn giản trên trang chủ của mỗi thư viện. Trong phần tiếp theo, chúng tôi sẽ làm việc thông qua một ví dụ với các thư viện HTTP và làm thế nào chúng ta có thể đóng gói và tổ chức code một cách hợp lý.

Đóng gói HTTP client Cách tốt nhất là có thể cung cấp các interface để tương tác với các thiết bị đầu cuối, hoặc service và ẩn tất cả các HTTP request và respond chi tiết ở cấp thấp hơn. Những lập trình viên khác không cần phải được tiếp xúc với các chi tiết cụ thể của việc thực hiện, mà vẫn có thể thay đổi / refactored mà không cần phải thay đổi các API công cộng. Chúng ta hãy xem làm thế nào chúng ta có thể làm điều này với một API. Chúng tôi sẽ sử dụng các API lcboapi.com. Nó cung cấp một giao diện để truy cập thông tin liên quan đến các loại đồ uống và các cửa hàng của LCBO (tập đoàn chính phủ ở Ontario, Canada, chịu trách nhiệm về bán lẻ và phân phối rượu cho tỉnh). Chúng ta sẽ làm việc với bốn class:

  • Lcbo::Products : Chúng tôi sẽ làm việc với interface để lấy thông tin chi tiết về một sản phẩm cụ thể.
  • Lcbo::ProductRequest : Xử lý các HTTP request cho một Product ID nhất định.
  • Lcbo::ProductResponse : Xử lý các HTTP respond và cách để xây dựng một Lcbo::Product.
  • Lcbo::Product : Chi tiết thực tế của sản phẩm.

Trước khi chúng ta đi sâu vào thực hiện, chúng ta hãy xem làm thế nào để sử dụng nó:

require_relative 'lib/lcbo'

key = ENV.fetch('LCBO_API_KEY')
product = Lcbo::Products.new(key).fetch(438457)

puts product.name
# Hopsta La Vista
puts product.tags.inspect
# ["hopsta", "la", "vista", "beer", "ale", "canada", "ontario", "longslice", "brewery", "inc", "can"]

Controller Class này hoạt động như public interface để lấy thông tin chi tiết về một hoặc nhiều sản phẩm LCBO. Tại thời điểm này, tôi chỉ thực hiện việc lấy, mà sẽ trả về chi tiết cho một sản phẩm duy nhất. Công việc của lớp này là xây dựng request và xử lý respond; nó kiểm soát flow và biết các cuộc gọi API phải được thực hiện trong lệnh gì.

module Lcbo
  require_relative 'product_request'
  require_relative 'product_response'

  class Products
    attr_accessor :key

    def initialize(key)
      @key = key
    end

    def fetch(product_id)
      connection = HTTP

      product_response = ProductRequest.new(key, product_id, connection).response
      fail LcboError, product_response.error_message unless product_response.success?

      product_response.product
    end
  end
end

Request Request class biết cách tạo một request duy nhất tới API (cơ bản là một thiết bị đầu cuối). Nó xây dựng các HTTP request, điền vào bất kỳ thông tin mà cần phải được bao gồm trong request đó.

module Lcbo
  class ProductRequest
    attr_reader :key, :product_id, :connection

    def initialize(key, product_id, connection)
      @key = key
      @product_id = product_id
      @connection = connection
    end

    def response
      http_response = connection
        .headers('Authorization' => "Token #{key}")
        .get(url)
      ProductResponse.new(http_response)
    end

    def url
      "https://lcboapi.com/products/#{product_id}"
    end
  end
end

Respond Các Response objects biết cách xử lý với những respond từ một API endpoint đơn. Nó có thể trả lại các thông tin chi tiết về respond hoặc xây dựng các đối tượng khác từ respond đó.

module Lcbo
  class ProductResponse
    attr_reader :http_response

    DEFAULT_ERROR_MESSAGE = 'There was an error retrieving product details.'.freeze

    def initialize(http_response)
      @http_response = http_response
    end

    def success?
      http_response.status == 200
    end

    def error_message
      data.fetch('message', DEFAULT_ERROR_MESSAGE)
    end

    def product
      Product.new(data.fetch('result'))
    end

    private

    def data
      http_response.parse(:json)
    end
  end
end

Product Class Nó được xây dựng bởi ProductResponse và đại diện cho một sản phẩm duy nhất trong API LCBO.

module Lcbo
  class Product
    attr_accessor :details

    def initialize(details)
      @details = details
    end

    def name
      details['name']
    end

    def tags
      details.fetch('tags', ').split(' ')
    end
  end
end

Logging Outgoing Traffic Lập trình sẽ dễ dàng khi mọi thứ hoạt động lần đầu tiên thử (không bao giờ xảy ra). API có thể khó vì các HTTP request có thể được dự kiến sẽ theo định dạng một cách rất cụ thể, với header này hoặc body kia. Sẽ rất khó nếu chỉ nhìn vào code Ruby để biết các HTTP request (và respond) thực sự trông như thế nào.

Gem httplog httplog là một gem mà đã vá hầu hết lỗi các thư viện HTTP chính, có thể hiển thi lưu lượng gửi đến và gửi đi đến giao diện điều khiển ($$stdout hoặc bất cứ nơi nào bạn muốn). Các thông tin cung cấp có thể giúp bạn nhận ra bạn viết một tiêu đề sai hoặc một cái gì đó đã mất. Đây là một ví dụ:

D, [2016-09-11T22:05:11.353063 #5345] DEBUG -- : [httplog] Sending: GET https://lcboapi.com/products/438457
D, [2016-09-11T22:05:11.353158 #5345] DEBUG -- : [httplog] Header: Authorization: Token MY_API_KEY
D, [2016-09-11T22:05:11.353184 #5345] DEBUG -- : [httplog] Header: Connection: close
D, [2016-09-11T22:05:11.353202 #5345] DEBUG -- : [httplog] Header: Host: lcboapi.com
D, [2016-09-11T22:05:11.353234 #5345] DEBUG -- : [httplog] Header: User-Agent: http.rb/2.0.3
D, [2016-09-11T22:05:11.353268 #5345] DEBUG -- : [httplog] Data:
D, [2016-09-11T22:05:11.353320 #5345] DEBUG -- : [httplog] Connecting: lcboapi.com:443
D, [2016-09-11T22:05:11.502537 #5345] DEBUG -- : [httplog] Status: 200
D, [2016-09-11T22:05:11.502658 #5345] DEBUG -- : [httplog] Benchmark: 0.1491872170008719 seconds
D, [2016-09-11T22:05:11.502815 #5345] DEBUG -- : [httplog] Header: Server: nginx/1.6.2
etc...

mitmproxy Một công cụ mạnh mẽ hơn là sử dụng một máy chủ proxy để theo dõi các request gửi đi của bạn và respond trả lời. mitmproxy là một công cụ tuyệt vời để có được cái nhìn về các lưu lượng HTTP ứng dụng của bạn. Để sử dụng mitmproxy, trước tiên bạn sẽ cần phải cài đặt nó và sau đó tải về một file giấy chứng nhậm có đuôi pem. Khi bạn đã có, bạn có thể khởi động máy chủ Rails của bạn hoặc chạy một kịch bản Ruby và thiết lập một var ENV để trỏ đến giấy chứng nhận tùy chỉnh SSL của bạn. Ví dụ

SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails s -p 3000

SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails c

SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem ruby demo.rb

Kết luận Việc giao tiếp với các API hoặc microservices có thể rất có giá trị. Nó cho phép thử nghiệm dễ dàng hơn, gói gọn các chi tiết ở mức độ thấp, và cung cấp phần code có thể tái sử dụng. Với httplogger và mitmproxy, chúng ta có thể có được cái nhìn chi tiết hơn về các request và respond.

0