Proper syntax for multipart/form-data using Godot4

Godot Version

4.2.2

Question

Currently trying to use HTTP Request node to send an API key and an image from user’s pc to an image upload site. I could get the API Key to the API fine, but that was using application/x-www-form-urlencoded. The issue is, when I switched to multipart/form-data, I can no longer get the api-key to send, I’m not sure if it’s a syntax error on my end, but there is no up to date syntax references for this issue. I’ll provide what I have, but it is clearly wrong, I just have no clue where at. Heres the code, you should be able to replicate it pretty easily. My syntax is for sure wrong, but there’s no real proper examples that I can find, at least for GDscript on this matter.

var http_request = HTTPRequest.new()
var api_key = "MYAPIKEY"
var send_url = "https://www.imghippo.com/v1/upload"
var file
var content = Image.load_from_file("user://20240520_160400.jpg")
var datalist:PackedByteArray = [] 
var boundary = "testboundary"
var fileb64
func _ready():

	http_request.request_completed.connect(_on_request_completed)
	add_child(http_request)

	
	
	send_image()
func send_image():
	datalist.append_array(('--testboundary').to_utf8_buffer())
	datalist.append_array(('Content-Disposition: form-data; name=file; filename=user://20240520_160400.jpg').to_utf8_buffer())
	datalist.append_array(('Content-Type: application/octet-stream').to_utf8_buffer())
	datalist.append_array(('').to_utf8_buffer())
	#file = Image.load_from_file('user://20240520_160400.jpg').get_data()
	#fileb64 = Marshalls.raw_to_base64(file)
	#datalist.append_array((content).)
	datalist.append_array(('--testboundary').to_utf8_buffer())
	datalist.append_array(('Content-Disposition: form-data; name=api_key;').to_utf8_buffer())
	datalist.append_array(('Content-Type: text/plain').to_utf8_buffer())
	datalist.append_array(('').to_utf8_buffer())
	datalist.append_array(('MYAPIKEY').to_utf8_buffer())
	datalist.append_array(('--testboundary--').to_utf8_buffer())
	datalist.append_array(('').to_utf8_buffer())
	var headers = ["Content-Type: multipart/form-data; boundary=testboundary"]
	http_request.request(send_url, headers, HTTPClient.METHOD_POST, str(datalist))
	
func _on_request_completed(results, response_code, headers, body):
	var requestbody = JSON.stringify(body.get_string_from_utf8())
	print(requestbody)

I tested this with a local server. I have not checked if the file is uploaded correctly or not. I’m also not sure how to send it as a binary encoding so I’m using a base64 one.

func send_image() -> void:
	# Create some random bytes to generate our boundary value
	var crypto = Crypto.new()
	var random_bytes = crypto.generate_random_bytes(16)
	var boundary = '--GODOT%s' % random_bytes.hex_encode()
	
	# Setup the header Content-Type with our bundary
	var headers = [
		'Content-Type: multipart/form-data;boundary=%s' % boundary
	]

	# Load the image and get the png buffer
	var image = load("res://letter.png").get_image() as Image
	var buffer = image.save_png_to_buffer()

	# Create our body
	var body = PackedStringArray()
	body.push_back("--{{boundary}}")
	body.push_back('Content-Disposition: form-data; name="api_key"')
	body.push_back('')
	body.push_back("MY_API_KEY") # The API key you have
	body.push_back("--{{boundary}}")
	body.push_back('Content-Disposition: form-data; name="image"; filename="my_image.png"')
	body.push_back('Content-Type: image/png')
	body.push_back('Content-Transfer-Encoding: base64') # Encode is base64
	body.push_back('')
	body.push_back(Marshalls.raw_to_base64(buffer)) # Convert the buffer to a base64 String
	body.push_back("--{{boundary}}--")

	# Finally, join the body with CRLF and insert our boundary
	var final_string = "\r\n".join(body).format({"boundary": boundary}, "{{_}}") 

	http_request.request('http://localhost:5173/upload', headers, HTTPClient.METHOD_POST, final_string)

HTTP request body uses CRLF (the \r\n part) to separate the lines.

Each field has this structure:

  • --boundary
  • Header of that field (Content-Disposition, Content-Type,…)
  • Empty line
  • The content of the field

The body ends with --boundary-- (notice the last -- characters)

The format is kinda explained here POST - HTTP | MDN

1 Like

Thank you so much for the reply! After seeing this and implementing it into my code, it still wasn’t working, I was getting a response from the server, but kept getting txt files back. The website I was using only supports binary data, and as far as how to get that in Godot, I am not sure. I was trying to send the bytes as a string, but it kept giving .BIN or .txt files back. My advice to anyone going through a similar situation, set it up just as @mrcdk did, but make sure the API you are using supports Base64 data transfer. My recommendation is imgbb, as it is doing everything I needed it for. Shout out to @mrcdk, I did over 8 hours of research trying to find out how to get the syntax properly working, but there just isn’t a lot of resources I could find, especially for Godot implementation. ALSO: if instead of wanting to access your res://, you can get image data straight from your directory. Here is the code:

var send_url = "YOURAPIURL" 

func send_image() -> void:
	# Create some random bytes to generate our boundary value
	var crypto = Crypto.new()
	var random_bytes = crypto.generate_random_bytes(16)
	var boundary = '--GODOT%s' % random_bytes.hex_encode()
	
	# Setup the header Content-Type with our boundary
	var headers = [
		'Content-Type: multipart/form-data;boundary=%s' % boundary



    #Get file as bytes,
    var image = FileAccess.get_file_as_bytes("user://YOURFILENAME.jpg")
    #proceed with code from @mrcdk, mine has a few tweaks, such as getting rid of filename, and using /jpeg instead of /png, and just make sure you tweak your form-data names to match your APIS proper parameters.
   
    var body = PackedStringArray()
	body.push_back("--{{boundary}}")
	body.push_back('Content-Disposition: form-data; name="key"')
	body.push_back('')
	body.push_back("YOURAPIKEY") # The API key you have
	body.push_back("--{{boundary}}")
	body.push_back('Content-Disposition: form-data; name="image"')
	body.push_back('Content-Type: image/jpeg')
	body.push_back('Content-Transfer-Encoding: base64') # base64 encode
	body.push_back('')
	body.push_back(Marshalls.raw_to_base64(image)) #Marshalls will encode your  bytes to base64, then should send just like original example
	body.push_back("--{{boundary}}--")
        var final_string = "\r\n".join(body).format({"boundary": boundary}, "{{_}}") 
	
        http_request.request(send_url, headers, HTTPClient.METHOD_POST, final_string)

Here’s how to send it as a binary encoded field:

func send_image_binary() -> void:
	# Create some random bytes to generate our boundary value
	var crypto = Crypto.new()
	var random_bytes = crypto.generate_random_bytes(16)
	var boundary = '--GODOT%s' % random_bytes.hex_encode()

	# Setup the header Content-Type with our bundary
	var headers = [
		'Content-Type: multipart/form-data;boundary=%s' % boundary
	]

	# Load the image and get the png buffer
	var image = load("res://letter.png").get_image() as Image
	var buffer = image.save_png_to_buffer()

	# Create our body
	var body = PackedByteArray()
	append_line(body, "--{{boundary}}".format({"boundary": boundary}, "{{_}}"))
	append_line(body, 'Content-Disposition: form-data; name="api_key"')
	append_line(body, '')
	append_line(body, "MY_API_KEY") # The API key you have
	append_line(body, "--{{boundary}}".format({"boundary": boundary}, "{{_}}"))
	append_line(body, 'Content-Disposition: form-data; name="image"; filename="my_image.png"')
	append_line(body, 'Content-Type: image/png')
	append_line(body, 'Content-Transfer-Encoding: binary')
	append_line(body, '')
	append_bytes(body, buffer)
	append_line(body, "--{{boundary}}--".format({"boundary": boundary}, "{{_}}"))

	http_request.request_raw('http://localhost:5173/upload', headers, HTTPClient.METHOD_POST, body)


func append_line(buffer:PackedByteArray, line:String) -> void:
	buffer.append_array(line.to_utf8_buffer())
	buffer.append_array('\r\n'.to_utf8_buffer())


func append_bytes(buffer:PackedByteArray, bytes:PackedByteArray) -> void:
	buffer.append_array(bytes)
	buffer.append_array('\r\n'.to_utf8_buffer())
1 Like