0

Upload image from unity client to rails server: problem and solution

I. Mở đầu

  • Mình cần upload ảnh từ ứng dụng viết bằng Unity lên server rails để làm avatar cho user

II. Server rails

  • Để upload ảnh trên server rails mình sử dụng:
    • AWS s3 để lưu trữ ảnh
    • paperclip gem dùng để upload ảnh

Các bạn có thể xem chi tiết về vấn đề này ở post: https://viblo.asia/nguyenhoa/posts/N0bDM6lov2X4

  • Tiếp theo mình viết 1 API để upload avatar:
  desc "Upload Avatar"
  params do
  requires :avatar, type: Rack::Multipart::UploadedFile
  end
  post "/upload_avatar" do
      current_user.avatar = ActionDispatch::Http::UploadedFile.new(permitted_params[:avatar])
      extension = permitted_params[:avatar][:type].split('/').last
      current_user.avatar_file_name = "#{current_user.user_name}.#{extension}"
      if current_user.save
          status :ok
          success_response! "upload_avatar_success"
      else
          Rails.logger.error current_user.errors.full_messages.join(", ")
          error_response! "upload_avatar_error", :unprocessable_entity
      end
  end
  • Sau khi test thử thành công API với postman => gửi pull request => được merge => ngủ ngon lành.

III.Problem

Và sáng hôm sau khi client Unity gọi API thì báo lỗi (!) what?

ArgumentError (unknown encoding name - "utf-8"):
  rack (1.6.4) lib/rack/multipart/parser.rb:211:in `find'
  rack (1.6.4) lib/rack/multipart/parser.rb:211:in `block in tag_multipart_encoding'
  rack (1.6.4) lib/rack/multipart/parser.rb:207:in `each'
  rack (1.6.4) lib/rack/multipart/parser.rb:207:in `tag_multipart_encoding'
  rack (1.6.4) lib/rack/multipart/parser.rb:75:in `block (2 levels) in parse'
  rack (1.6.4) lib/rack/multipart/parser.rb:250:in `get_data'
  rack (1.6.4) lib/rack/multipart/parser.rb:74:in `block in parse'
  rack (1.6.4) lib/rack/multipart/parser.rb:56:in `loop'
  rack (1.6.4) lib/rack/multipart/parser.rb:56:in `parse'
  rack (1.6.4) lib/rack/multipart.rb:25:in `parse_multipart'
  rack (1.6.4) lib/rack/request.rb:375:in `parse_multipart'
  rack (1.6.4) lib/rack/request.rb:207:in `POST'

Sau khi tìm hiểu thì Unity3d code generate ra multipart http request như sau:

POST path/to/upload HTTP/1.1
X-Unity-Version: 4.6.7f1
Content-Type: multipart/form-data; boundary="DCUxGP7YJupYAgWvtEzroP1c0RShKSqE3o5mpsiG"
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.1; A210 Build/JRO03H)
Host: *****
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 1673131

--DCUxGP7YJupYAgWvtEzroP1c0RShKSqE3o5mpsiG
Content-Type: text/plain; charset="utf-8"
Content-disposition: form-data; name="user_sid"

bbf14f82-d2aa-4c07-9fb8-ca6714a7ea97
--DCUxGP7YJupYAgWvtEzroP1c0RShKSqE3o5mpsiG
Content-Type: image/png; charset=UTF-8
Content-disposition: form-data; name="file"; filename="b67879ed-bfed-4491-a8cc-f99cca769f94.png"

trong khi đó brower hay các nền tảng khác sẽ generate ra như sau:

The Content-Type entity-header field indicates the media type of the
   entity-body sent to the recipient or, in the case of the HEAD method,
   the media type that would have been sent had the request been a GET.

       Content-Type   = "Content-Type" ":" media-type

   Media types are defined in section 3.7. An example of the field is

       Content-Type: text/html; charset=ISO-8859-4

   Further discussion of methods for identifying the media type of an
   entity is provided in section 7.2.1.

Và có thể thấy là Unity3d đã generate ra invalid field "Content-Type" mà gem "Rack" không thể parse được.

IV. Solution

  • Với phiên bản 1.6.4 hiện tại thì gem rack vẫn không thể parse được http request trên của unity3d. Vì vậy để xử lí vấn đề này ta cần override lại method "tag_multipart_encoding" trong class Parse của gem này.
  • bạn tạo file config/initializers/rack.rb như sau:
require 'rack/utils'

module Rack
  module Multipart
    class MultipartPartLimitError < Errno::EMFILE; end

    class Parser
      def tag_multipart_encoding(filename, content_type, name, body)
        name = name.to_s
        encoding = Encoding::UTF_8

        name.force_encoding(encoding)

        return if filename

        if content_type
          list         = content_type.split(';')
          type_subtype = list.first
          type_subtype.strip!
          if TEXT_PLAIN == type_subtype
            rest         = list.drop 1
            rest.each do |param|
              k,v = param.split('=', 2)
              k.strip!
              v.strip!
              v = v[1..-2] if v[0] == '"' && v[-1] == '"'
              encoding = Encoding.find v if k == CHARSET
            end
          end
        end

        name.force_encoding(encoding)
        body.force_encoding(encoding)
      end
    end
  end
end

Và bùm. vấn đề đã được giải quyết 😄. Dòng duy nhất cần cần thêm là:

v = v[1..-2] if v[0] == '"' && v[-1] == '"'

Issue này vẫn tồn tại ở Rack version 1.6.4 nhưng khả năng sẽ được fix trong version tiếp theo. Hi vong Rack sớm release version mới!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí