I encountered the problem of encapsulating the Media type multipart/form-data
when writing a generic HTTP component. This article briefly introduces the definition, application and elementary implementation of the media type multipart/form-data
in the HTTP protocol.
The media type multipart/form-information
follows the multipart MIME
data stream definition (which can be constitute in Department 5.1 - RFC2046), which roughly means that the data trunk of the media type multipart/course-data
consists of multiple parts separated by a fixed Boundary.
multipart/form-data request body layout
The layout of the multipart/form-information
request body is as follows.
1 2 iii four 5 6 7 8 9 10 xi 12 13 14 xv xvi 17 xviii 19 20 | # 请求头 - 这个是必须的,需要指定Content-Type为multipart/course-data,指定唯一边界值 Content-Blazon: multipart/grade-information; boundary=${Boundary} # 请求体 --${Boundary} Content-Disposition: grade-data; name="name of file" Content-Blazon: awarding/octet-stream bytes of file --${Boundary} Content-Disposition: form-data; name="name of pdf"; filename="pdf-file.pdf" Content-Type: application/octet-stream bytes of pdf file --${Purlieus} Content-Disposition: form-data; name="fundamental" Content-Blazon: text/manifestly;charset=UTF-8 text encoded in UTF-8 --${Purlieus}-- |
The virtually obvious differences between the media blazon multipart/course-data
versus other media types such as application/ten-world wide web-course-urlencoded
are
- The Content-Type attribute of the request header, in addition to specifying
multipart/form-data
, also requires the definition of the boundary parameter - The request line data in the request torso is composed of multiple parts, and the value pattern of the boundary parameter - ${Boundary} is used to separate each private division
- The request header
Content-Disposition: course-information; name="${PART_NAME}";
must exist present in each section, where ${PART_NAME} needs to exist URL encoded, and the filename field tin be used to indicate the proper name of the file, but it is less binding than the name attribute (equally there is no confirmation that the local file is available or objectionable) - Each part tin define
Content-Blazon
and the data body of that part separately - The request body ends with the value pattern
-${Boundary}--
of the boundary
parameter
RFC7578 mentions two multipart/course-data expired use, one is the use of Content-Transfer-Encoding request header, here also do not expand its utilise, the second is the request trunk of a unmarried form attribute manual of multiple binary file way recommended to switch to multipart/mixed (a "name" corresponds to multiple binary file scenario)
Special.
- If the content of a function is text, its Content-Blazon is text/plain, you can specify the corresponding character set, such as Content-Type: text/plain;charset=UTF-viii
- The default charset tin be specified via the charset aspect, which is used as follows.
one 2 3 4 5 half-dozen 7 eight | Content-Disposition: form-information; name="_charset_" UTF-8 --ABCDE-- Content-Disposition: form-data; name="field" ...text encoded in UTF-viii... ABCDE-- |
Boundary parameter value statute
The Boundary
parameter takes the following value statute.
- The value of the Purlieus must brainstorm with a double horizontal bar – in the middle of the English, this – is called the leading hyphen
- The value of the Boundary must not contain more than seventy characters in addition to the leading hyphen.
- The value of the Boundary must not contain characters that are disabled by the HTTP protocol or the URL, such every bit the colon: etc.
- Each – ${Boundary} before the default mandatory must exist CRLF, if a part of the text type request body ends with CRLF, and so in the request body of the secondary system format, at that place must exist two CRLF explicitly, if a part of the request torso does not terminate with CRLF, can merely exist a CRLF, these ii cases are called the separator of the explicit type and implicit type, said more abstract, meet the following example.
1 ii 3 4 5 6 vii eight nine 10 11 12 xiii 14 15 16 17 18 19 20 21 | # 请求头 Content-blazon: multipart/data; boundary="--abcdefg" --abcdefg Content-Disposition: form-data; proper noun="x" Content-type: text/patently; charset=ascii It does NOT end with a linebreak # <=== 这里没有CRLF,隐式类型 --abcdefg Content-Disposition: form-data; name="y" Content-type: text/plain; charset=ascii It DOES end with a linebreak # <=== 这里有CRLF,显式类型 --abcdefg ## 直观看隐式类型的CRLF It does Not end with a linebreak CRLF --abcdefg ## 直观看显式类型的CRLF It DOES terminate with a linebreak CRLF CRLF --abcdefg |
Implementing POST requests for multipart/form-information media types
Here just for the depression JDK version of HttpURLConnection and high JDK version of the built-in HttpClient to write multipart/grade-data media type of POST requests HTTP client, others such equally custom Socket implementation can be completed forth similar lines. Commencement introduce org.springframework.boot:spring-boot-starter-web:2.six.0 to do a simple controller method.
1 ii 3 4 5 6 seven eight | @RestController public class TestController { @PostMapping ( path = "/test" ) public ResponseEntity <?> exam ( MultipartHttpServletRequest request ) { return ResponseEntity . ok ( "ok" ); } } |
Postman's simulated request is as follows.
The request parameters obtained by the backend controller are as follows.
The client written later can call this interface directly for debugging.
Module that encapsulates the conversion of asking bodies into byte containers
Here the boundary values are all implemented explicitly, and the boundary values are generated directly with a stock-still prefix plus the UUID. Some simplifications are made in the simple implementation.
- Only text form data and binary (file) form data are considered for submission
- Based on the previous bespeak, each department explicitly specifies Content-Type as the request header
- Text encoding is fixed to UTF-8
Write a MultipartWriter.
1 two iii 4 five half dozen 7 viii 9 10 11 12 xiii 14 fifteen 16 17 eighteen nineteen 20 21 22 23 24 25 26 27 28 29 xxx 31 32 33 34 35 36 37 38 39 xl 41 42 43 44 45 46 47 48 49 l 51 52 53 54 55 56 57 58 59 sixty 61 62 63 64 65 66 67 68 69 lxx 71 72 73 74 75 76 77 78 79 fourscore 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | public course MultipartWriter { private static final Charset DEFAULT_CHARSET = StandardCharsets . UTF_8 ; private static final byte [] FIELD_SEP = ": " . getBytes ( StandardCharsets . ISO_8859_1 ); private static final byte [] CR_LF = "\r\north" . getBytes ( StandardCharsets . ISO_8859_1 ); private static final String TWO_HYPHENS_TEXT = "--" ; individual static final byte [] TWO_HYPHENS = TWO_HYPHENS_TEXT . getBytes ( StandardCharsets . ISO_8859_1 ); private static concluding Cord CONTENT_DISPOSITION_KEY = "Content-Disposition" ; private static final String CONTENT_TYPE_KEY = "Content-Type" ; private static final String DEFAULT_CONTENT_TYPE = "multipart/form-information; boundary=" ; individual static final String DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream" ; individual static final String DEFAULT_TEXT_CONTENT_TYPE = "text/plain;charset=UTF-viii" ; private static concluding String DEFAULT_CONTENT_DISPOSITION_VALUE = "form-data; name=\"%s\"" ; private static final String FILE_CONTENT_DISPOSITION_VALUE = "form-information; name=\"%south\"; filename=\"%due south\"" ; private final Map < String , Cord > headers = new HashMap <>( eight ); private final List < AbstractMultipartPart > parts = new ArrayList <>(); private final String boundary ; private MultipartWriter ( Cord purlieus ) { this . boundary = Objects . isNull ( purlieus ) ? TWO_HYPHENS_TEXT + UUID . randomUUID (). toString (). supervene upon ( "-" , "" ) : purlieus ; this . headers . put ( CONTENT_TYPE_KEY , DEFAULT_CONTENT_TYPE + this . boundary ); } public static MultipartWriter newMultipartWriter ( String boundary ) { render new MultipartWriter ( purlieus ); } public static MultipartWriter newMultipartWriter () { render new MultipartWriter ( null ); } public MultipartWriter addHeader ( String key , String value ) { if (! CONTENT_TYPE_KEY . equalsIgnoreCase ( key )) { headers . put ( key , value ); } return this ; } public MultipartWriter addTextPart ( String name , String text ) { parts . add ( new TextPart ( String . format ( DEFAULT_CONTENT_DISPOSITION_VALUE , name ), DEFAULT_TEXT_CONTENT_TYPE , this . boundary , text )); return this ; } public MultipartWriter addBinaryPart ( String name , byte [] bytes ) { parts . add ( new BinaryPart ( String . format ( DEFAULT_CONTENT_DISPOSITION_VALUE , name ), DEFAULT_BINARY_CONTENT_TYPE , this . boundary , bytes )); return this ; } public MultipartWriter addFilePart ( String proper name , File file ) { parts . add ( new FilePart ( String . format ( FILE_CONTENT_DISPOSITION_VALUE , proper noun , file . getName ()), DEFAULT_BINARY_CONTENT_TYPE , this . boundary , file )); return this ; } private static void writeHeader ( Cord key , String value , OutputStream out ) throws IOException { writeBytes ( key , out ); writeBytes ( FIELD_SEP , out ); writeBytes ( value , out ); writeBytes ( CR_LF , out ); } private static void writeBytes ( Cord text , OutputStream out ) throws IOException { out . write ( text . getBytes ( DEFAULT_CHARSET )); } individual static void writeBytes ( byte [] bytes , OutputStream out ) throws IOException { out . write ( bytes ); } interface MultipartPart { void writeBody ( OutputStream os ) throws IOException ; } @RequiredArgsConstructor public static abstract class AbstractMultipartPart implements MultipartPart { protected final String contentDispositionValue ; protected terminal String contentTypeValue ; protected concluding Cord boundary ; protected String getContentDispositionValue () { return contentDispositionValue ; } protected String getContentTypeValue () { return contentTypeValue ; } protected Cord getBoundary () { return boundary ; } public concluding void write ( OutputStream out ) throws IOException { writeBytes ( TWO_HYPHENS , out ); writeBytes ( getBoundary (), out ); writeBytes ( CR_LF , out ); writeHeader ( CONTENT_DISPOSITION_KEY , getContentDispositionValue (), out ); writeHeader ( CONTENT_TYPE_KEY , getContentTypeValue (), out ); writeBytes ( CR_LF , out ); writeBody ( out ); writeBytes ( CR_LF , out ); } } public static grade TextPart extends AbstractMultipartPart { private final String text ; public TextPart ( String contentDispositionValue , String contentTypeValue , String boundary , Cord text ) { super ( contentDispositionValue , contentTypeValue , boundary ); this . text = text ; } @Override public void writeBody ( OutputStream os ) throws IOException { os . write ( text . getBytes ( DEFAULT_CHARSET )); } @Override protected String getContentDispositionValue () { render contentDispositionValue ; } @Override protected String getContentTypeValue () { render contentTypeValue ; } } public static course BinaryPart extends AbstractMultipartPart { private final byte [] content ; public BinaryPart ( String contentDispositionValue , String contentTypeValue , String purlieus , byte [] content ) { super ( contentDispositionValue , contentTypeValue , boundary ); this . content = content ; } @Override public void writeBody ( OutputStream out ) throws IOException { out . write ( content ); } } public static course FilePart extends AbstractMultipartPart { private concluding File file ; public FilePart ( String contentDispositionValue , String contentTypeValue , String boundary , File file ) { super ( contentDispositionValue , contentTypeValue , boundary ); this . file = file ; } @Override public void writeBody ( OutputStream out ) throws IOException { try ( InputStream in = new FileInputStream ( file )) { final byte [] buffer = new byte [ 4096 ]; int l ; while (( l = in . read ( buffer )) != - ane ) { out . write ( buffer , 0 , fifty ); } out . flush (); } } } public void forEachHeader ( BiConsumer < String , String > consumer ) { headers . forEach ( consumer ); } public void write ( OutputStream out ) throws IOException { if (! parts . isEmpty ()) { for ( AbstractMultipartPart part : parts ) { part . write ( out ); } } writeBytes ( TWO_HYPHENS , out ); writeBytes ( this . boundary , out ); writeBytes ( TWO_HYPHENS , out ); writeBytes ( CR_LF , out ); } } |
This form has encapsulated three different types of fractional request body implementations. The forEachHeader() method is used to iterate through the request headers, and the final write() method is used to write the request trunk to the OutputStream.
HttpURLConnection implementation
The implementation lawmaking is as follows (minimal implementation but, without consideration of error tolerance and exception handling).
ane 2 iii iv 5 6 7 eight 9 10 11 12 13 14 15 16 17 eighteen 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class HttpURLConnectionApp { private static last String URL = "http://localhost:9099/examination" ; public static void main ( String [] args ) throws Exception { MultipartWriter author = MultipartWriter . newMultipartWriter (); author . addTextPart ( "proper name" , "throwable" ) . addTextPart ( "domain" , "vlts.cn" ) . addFilePart ( "ico" , new File ( "I:\\doge_favicon.ico" )); DataOutputStream requestPrinter = new DataOutputStream ( System . out ); writer . write ( requestPrinter ); HttpURLConnection connection = ( HttpURLConnection ) new java . net . URL ( URL ). openConnection (); connectedness . setRequestMethod ( "POST" ); connexion . addRequestProperty ( "Connection" , "Go on-Alive" ); // 设置请求头 writer . forEachHeader ( connexion :: addRequestProperty ); connection . setDoInput ( true ); connection . setDoOutput ( true ); connection . setConnectTimeout ( 10000 ); connectedness . setReadTimeout ( 10000 ); DataOutputStream out = new DataOutputStream ( connection . getOutputStream ()); // 设置请求体 writer . write ( out ); StringBuilder builder = new StringBuilder (); BufferedReader reader = new BufferedReader ( new InputStreamReader ( connectedness . getInputStream (), StandardCharsets . UTF_8 )); String line ; while ( Objects . nonNull ( line = reader . readLine ())) { builder . append ( line ); } int responseCode = connection . getResponseCode (); reader . close (); out . shut (); connexion . disconnect (); System . out . printf ( "响应码:%d,响应内容:%s\n" , responseCode , builder ); } } |
Implementation response results.
You tin can effort calculation two lines of code to impress the asking body.
1 2 iii 4 five half-dozen | MultipartWriter writer = MultipartWriter . newMultipartWriter (); writer . addTextPart ( "name" , "throwable" ) . addTextPart ( "domain" , "vlts.cn" ) . addFilePart ( "ico" , new File ( "I:\\doge_favicon.ico" )); DataOutputStream requestPrinter = new DataOutputStream ( System . out ); writer . write ( requestPrinter ); |
Console output as follows.
JDK congenital-in HttpClient implementation
JDK11+ built-in HTTP client implementation, the specific archway is java.net.http.HttpClient, the implementation code is as follows.
1 2 three four five 6 seven 8 9 ten xi 12 13 14 xv xvi 17 18 xix 20 21 22 23 | public class HttpClientApp { private static final Cord URL = "http://localhost:9099/exam" ; public static void main ( String [] args ) throws Exception { HttpClient httpClient = HttpClient . newBuilder () . connectTimeout ( Duration . of ( 10 , ChronoUnit . SECONDS )) . build (); MultipartWriter writer = MultipartWriter . newMultipartWriter (); writer . addTextPart ( "proper noun" , "throwable" ) . addTextPart ( "domain" , "vlts.cn" ) . addFilePart ( "ico" , new File ( "I:\\doge_favicon.ico" )); ByteArrayOutputStream out = new ByteArrayOutputStream (); author . write ( out ); HttpRequest . Architect requestBuilder = HttpRequest . newBuilder (); writer . forEachHeader ( requestBuilder :: header ); HttpRequest request = requestBuilder . uri ( URI . create ( URL )) . method ( "Post" , HttpRequest . BodyPublishers . ofByteArray ( out . toByteArray ())) . build (); HttpResponse < String > response = httpClient . send ( request , HttpResponse . BodyHandlers . ofString ()); Organization . out . printf ( "响应码:%d,响应内容:%s\north" , response . statusCode (), response . body ()); } } |
The congenital-in HTTP components are almost all using Reactive programming model, using a relatively depression-level API, which is more than flexible merely not every bit easy to use.
Summary
The media blazon multipart/class-data is commonly used in HTTP requests nether the POST method, and is relatively uncommon as an HTTP response.
0 Response to "How To Set Content Type Of File For Multi-part Form Data"
Post a Comment