diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java index 1d03ad22bb..1e8a1a4204 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java @@ -15,10 +15,6 @@ */ package org.asynchttpclient.request.body.multipart; -import jakarta.activation.MimetypesFileTypeMap; - -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.Charset; import static org.asynchttpclient.util.MiscUtils.withDefault; @@ -28,16 +24,6 @@ */ public abstract class FileLikePart extends PartBase { - private static final MimetypesFileTypeMap MIME_TYPES_FILE_TYPE_MAP; - - static { - try (InputStream is = FileLikePart.class.getResourceAsStream("ahc-mime.types")) { - MIME_TYPES_FILE_TYPE_MAP = new MimetypesFileTypeMap(is); - } catch (IOException e) { - throw new ExceptionInInitializerError(e); - } - } - /** * Default content encoding of file attachments. */ @@ -63,7 +49,7 @@ protected FileLikePart(String name, String contentType, Charset charset, String } private static String computeContentType(String contentType, String fileName) { - return contentType != null ? contentType : MIME_TYPES_FILE_TYPE_MAP.getContentType(withDefault(fileName, "")); + return contentType != null ? contentType : MimeTypes.getContentType(withDefault(fileName, "")); } public String getFileName() { diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/MimeTypes.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/MimeTypes.java new file mode 100644 index 0000000000..69a29f80d4 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/MimeTypes.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.request.body.multipart; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Maps file extensions to content types using the bundled {@code ahc-mime.types} resource. + * + *

This is a self-contained replacement for {@code jakarta.activation.MimetypesFileTypeMap}, which was + * previously used solely to resolve the content type of {@link FileLikePart}s. The lookup mirrors the + * {@code MimetypesFileTypeMap} contract: the extension is the substring after the last {@code '.'}, and an + * unknown or missing extension resolves to {@value #DEFAULT_CONTENT_TYPE}. Unlike the original, the lookup is + * case-insensitive and relies exclusively on the bundled resource, so detection is deterministic across + * machines and classpaths. + */ +final class MimeTypes { + + static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; + + /** + * Lower-cased file extension to content type. Built once at class load from {@code ahc-mime.types}. + */ + private static final Map EXTENSION_TO_CONTENT_TYPE; + + static { + Map map = new HashMap<>(); + // The MimetypesFileTypeMap format: '#' comments, blank lines, and whitespace-delimited + // "type ext1 ext2 ..." entries. A later mapping for the same extension wins, matching the + // original Hashtable-based implementation. + try (InputStream is = MimeTypes.class.getResourceAsStream("ahc-mime.types"); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.charAt(0) == '#') { + continue; + } + String[] tokens = line.split("\\s+"); + for (int i = 1; i < tokens.length; i++) { + map.put(tokens[i].toLowerCase(Locale.ROOT), tokens[0]); + } + } + } catch (IOException e) { + throw new ExceptionInInitializerError(e); + } + EXTENSION_TO_CONTENT_TYPE = Collections.unmodifiableMap(map); + } + + private MimeTypes() { + // Prevent outside initialization + } + + /** + * Resolves the content type for the given file name based on its extension. + * + * @param fileName the file name (may include a path; only the part after the last {@code '.'} is used) + * @return the mapped content type, or {@value #DEFAULT_CONTENT_TYPE} if the extension is absent or unknown + */ + static String getContentType(String fileName) { + int dot = fileName.lastIndexOf('.'); + if (dot < 0) { + return DEFAULT_CONTENT_TYPE; + } + String extension = fileName.substring(dot + 1); + if (extension.isEmpty()) { + return DEFAULT_CONTENT_TYPE; + } + return EXTENSION_TO_CONTENT_TYPE.getOrDefault(extension.toLowerCase(Locale.ROOT), DEFAULT_CONTENT_TYPE); + } +} diff --git a/client/src/main/resources/org/asynchttpclient/request/body/multipart/ahc-mime.types b/client/src/main/resources/org/asynchttpclient/request/body/multipart/ahc-mime.types index 4ec2dfc6b8..b94996799e 100644 --- a/client/src/main/resources/org/asynchttpclient/request/body/multipart/ahc-mime.types +++ b/client/src/main/resources/org/asynchttpclient/request/body/multipart/ahc-mime.types @@ -1,6 +1,6 @@ # This file maps Internet media types to unique file extension(s). # Although created for httpd, this file is used by many software systems -# and has been placed in the public domain for unlimited redisribution. +# and has been placed in the public domain for unlimited redistribution. # # The table below contains both registered and (common) unregistered types. # A type that has no unique extension can be ignored -- they are listed @@ -94,6 +94,7 @@ application/ecmascript ecma # application/edi-consent # application/edi-x12 # application/edifact +# application/efi # application/emergencycalldata.comment+xml # application/emergencycalldata.deviceinfo+xml # application/emergencycalldata.providerinfo+xml @@ -111,10 +112,9 @@ application/exi exi # application/fastsoap # application/fdt+xml # application/fits -# application/font-sfnt application/font-tdpfr pfr -application/font-woff woff # application/framework-attributes+xml +# application/geo+json application/gml+xml gml application/gpx+xml gpx application/gxf gxf @@ -142,7 +142,7 @@ application/ipfix ipfix application/java-archive jar application/java-serialized-object ser application/java-vm class -application/javascript js +# application/javascript # application/jose # application/jose+json # application/jrd+json @@ -156,6 +156,7 @@ application/jsonml+json jsonml # application/kpml-request+xml # application/kpml-response+xml # application/ld+json +# application/lgr+xml # application/link-format # application/load-control+xml application/lost+xml lostxml @@ -358,6 +359,7 @@ application/vnd.3gpp.pic-bw-large plb application/vnd.3gpp.pic-bw-small psb application/vnd.3gpp.pic-bw-var pvb # application/vnd.3gpp.sms +# application/vnd.3gpp.sms+xml # application/vnd.3gpp.srvcc-ext+xml # application/vnd.3gpp.srvcc-info+xml # application/vnd.3gpp.state-and-event-info+xml @@ -365,6 +367,7 @@ application/vnd.3gpp.pic-bw-var pvb # application/vnd.3gpp2.bcmcsinfo+xml # application/vnd.3gpp2.sms application/vnd.3gpp2.tcap tcap +# application/vnd.3lightssoftware.imagescal application/vnd.3m.post-it-notes pwn application/vnd.accpac.simply.aso aso application/vnd.accpac.simply.imp imp @@ -383,6 +386,7 @@ application/vnd.ahead.space ahead application/vnd.airzip.filesecure.azf azf application/vnd.airzip.filesecure.azs azs application/vnd.amazon.ebook azw +# application/vnd.amazon.mobi8-ebook application/vnd.americandynamics.acc acc application/vnd.amiga.ami ami # application/vnd.amundsen.maze+xml @@ -419,6 +423,7 @@ application/vnd.businessobjects rep # application/vnd.cendio.thinlinc.clientconf # application/vnd.century-systems.tcp_stream application/vnd.chemdraw+xml cdxml +# application/vnd.chess-pgn application/vnd.chipnuts.karaoke-mmd mmd application/vnd.cinderella cdy # application/vnd.cirpack.isdn-ext @@ -432,9 +437,11 @@ application/vnd.cluetrust.cartomobile-config-pkg c11amz # application/vnd.collection+json # application/vnd.collection.doc+json # application/vnd.collection.next+json +# application/vnd.comicbook+zip # application/vnd.commerce-battelle application/vnd.commonspace csp application/vnd.contact.cmsg cdbcmsg +# application/vnd.coreos.ignition+json application/vnd.cosmocaller cmc application/vnd.crick.clicker clkx application/vnd.crick.clicker.keyboard clkk @@ -578,6 +585,7 @@ application/vnd.genomatix.tuxedo txd # application/vnd.geo+json # application/vnd.geocube+xml application/vnd.geogebra.file ggb +application/vnd.geogebra.slides ggs application/vnd.geogebra.tool ggt application/vnd.geometry-explorer gex gre application/vnd.geonext gxt @@ -886,6 +894,8 @@ application/vnd.olpc-sugar xo application/vnd.oma.dd2+xml dd2 # application/vnd.oma.drm.risd+xml # application/vnd.oma.group-usage-list+xml +# application/vnd.oma.lwm2m+json +# application/vnd.oma.lwm2m+tlv # application/vnd.oma.pal+xml # application/vnd.oma.poc.detailed-progress-report+xml # application/vnd.oma.poc.final-report+xml @@ -899,6 +909,7 @@ application/vnd.oma.dd2+xml dd2 # application/vnd.omads-file+xml # application/vnd.omads-folder+xml # application/vnd.omaloc-supl-init +# application/vnd.onepager # application/vnd.openblox.game+xml # application/vnd.openblox.game-binary # application/vnd.openeye.oeb @@ -1013,6 +1024,7 @@ application/vnd.pvi.ptid1 ptid # application/vnd.pwg-multiplexed # application/vnd.pwg-xhtml-print+xml # application/vnd.qualcomm.brew-app-res +# application/vnd.quarantainenet application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb # application/vnd.quobject-quoxdocument # application/vnd.radisys.moml+xml @@ -1032,6 +1044,7 @@ application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb # application/vnd.radisys.msml-dialog-transform+xml # application/vnd.rainstor.data # application/vnd.rapid +# application/vnd.rar application/vnd.realvnc.bed bed application/vnd.recordare.musicxml mxl application/vnd.recordare.musicxml+xml musicxml @@ -1077,6 +1090,7 @@ application/vnd.smart.teacher teacher application/vnd.solent.sdkm+xml sdkm sdkd application/vnd.spotfire.dxp dxp application/vnd.spotfire.sfs sfs +application/vnd.sqlite3 sqlite sqlite3 # application/vnd.sss-cod # application/vnd.sss-dtf # application/vnd.sss-ntf @@ -1146,6 +1160,7 @@ application/vnd.uoml+xml uoml application/vnd.vcx vcx # application/vnd.vd-study # application/vnd.vectorworks +# application/vnd.vel+json # application/vnd.verimatrix.vcas # application/vnd.vidsoft.vidconference application/vnd.visio vsd vst vss vsw @@ -1199,6 +1214,7 @@ application/vnd.zul zir zirz application/vnd.zzazz.deck+xml zaz application/voicexml+xml vxml # application/vq-rtcpxr +application/wasm wasm # application/watcherinfo+xml # application/whoispp-query # application/whoispp-response @@ -1246,12 +1262,10 @@ application/x-font-bdf bdf application/x-font-ghostscript gsf # application/x-font-libgrx application/x-font-linux-psf psf -application/x-font-otf otf application/x-font-pcf pcf application/x-font-snf snf # application/x-font-speedo # application/x-font-sunos-news -application/x-font-ttf ttf ttc application/x-font-type1 pfa pfb pfm afm # application/x-font-vfont application/x-freearc arc @@ -1428,7 +1442,7 @@ audio/mp4 m4a mp4a audio/mpeg mpga mp2 mp2a mp3 m2a m3a # audio/mpeg4-generic # audio/musepack -audio/ogg oga ogg spx +audio/ogg oga ogg spx opus # audio/opus # audio/parityfec # audio/pcma @@ -1519,17 +1533,32 @@ chemical/x-cml cml chemical/x-csml csml # chemical/x-pdb chemical/x-xyz xyz +font/collection ttc +font/otf otf +# font/sfnt +font/ttf ttf +font/woff woff +font/woff2 woff2 +image/avif avif image/bmp bmp image/cgm cgm +# image/dicom-rle +# image/emf # image/example # image/fits image/g3fax g3 image/gif gif +image/heic heic +image/heic-sequence heics +image/heif heif +image/heif-sequence heifs image/ief ief +# image/jls # image/jp2 image/jpeg jpeg jpg jpe # image/jpm # image/jpx +image/jxl jxl image/ktx ktx # image/naplps image/png png @@ -1572,6 +1601,7 @@ image/vnd.wap.wbmp wbmp image/vnd.xiff xif # image/vnd.zbrush.pcx image/webp webp +# image/wmf image/x-3ds 3ds image/x-cmu-raster ras image/x-cmx cmx @@ -1611,6 +1641,7 @@ message/rfc822 eml mime # message/vnd.si.simp # message/vnd.wfa.wsc # model/example +# model/gltf+json model/iges igs iges model/mesh msh mesh silo model/vnd.collada+xml dae @@ -1621,7 +1652,7 @@ model/vnd.gdl gdl # model/vnd.gs.gdl model/vnd.gtw gtw # model/vnd.moml+xml -model/vnd.mts mts +# model/vnd.mts # model/vnd.opengex # model/vnd.parasolid.transmit.binary # model/vnd.parasolid.transmit.text @@ -1664,7 +1695,7 @@ text/csv csv # text/fwdred # text/grammar-ref-list text/html html htm -# text/javascript +text/javascript js mjs # text/jcr-cnd # text/markdown # text/mizar @@ -1675,6 +1706,7 @@ text/plain txt text conf def list log in # text/provenance-notation # text/prs.fallenstein.rst text/prs.lines.tag dsc +# text/prs.prop.logic # text/raptorfec # text/red # text/rfc822-headers @@ -1759,7 +1791,7 @@ video/jpm jpm jpgm video/mj2 mj2 mjp2 # video/mp1s # video/mp2p -# video/mp2t +video/mp2t ts m2t m2ts mts video/mp4 mp4 mp4v mpg4 # video/mp4v-es video/mpeg mpeg mpg mpe m1v m2v diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MimeTypesTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MimeTypesTest.java new file mode 100644 index 0000000000..ec9de5e076 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MimeTypesTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.request.body.multipart; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MimeTypesTest { + + @Test + void resolvesKnownExtensions() { + assertEquals("image/png", MimeTypes.getContentType("image.png")); + assertEquals("text/html", MimeTypes.getContentType("data.html")); + assertEquals("image/jpeg", MimeTypes.getContentType("photo.jpeg")); + } + + @Test + void lookupIsCaseInsensitive() { + assertEquals("image/png", MimeTypes.getContentType("IMAGE.PNG")); + assertEquals("text/html", MimeTypes.getContentType("Data.Html")); + } + + @Test + void usesExtensionAfterLastDot() { + assertEquals("image/png", MimeTypes.getContentType("my.archive.png")); + } + + @Test + void resolvesModernExtensions() { + // Added by the upstream Apache httpd mime.types refresh. + assertEquals("application/wasm", MimeTypes.getContentType("module.wasm")); + assertEquals("image/avif", MimeTypes.getContentType("picture.avif")); + assertEquals("image/jxl", MimeTypes.getContentType("picture.jxl")); + assertEquals("font/woff2", MimeTypes.getContentType("font.woff2")); + } + + @Test + void usesModernizedContentTypes() { + // IANA-preferred types replacing the legacy mappings (RFC 9239, RFC 8081). + assertEquals("text/javascript", MimeTypes.getContentType("app.js")); + assertEquals("font/woff", MimeTypes.getContentType("font.woff")); + assertEquals("font/ttf", MimeTypes.getContentType("font.ttf")); + assertEquals("font/otf", MimeTypes.getContentType("font.otf")); + } + + @Test + void unknownExtensionFallsBackToDefault() { + assertEquals(MimeTypes.DEFAULT_CONTENT_TYPE, MimeTypes.getContentType("file.zzz")); + } + + @Test + void missingExtensionFallsBackToDefault() { + assertEquals(MimeTypes.DEFAULT_CONTENT_TYPE, MimeTypes.getContentType("noextension")); + assertEquals(MimeTypes.DEFAULT_CONTENT_TYPE, MimeTypes.getContentType("trailingdot.")); + assertEquals(MimeTypes.DEFAULT_CONTENT_TYPE, MimeTypes.getContentType("")); + } + + @Test + void fileLikePartDerivesContentTypeFromFileName() { + // The wiring formerly provided by jakarta.activation: with no explicit content type a part derives + // one from its file name, and falls back to the default when the file name carries no usable extension. + assertEquals("image/png", new ByteArrayPart("p", new byte[0], null, null, "x.png").getContentType()); + assertEquals(MimeTypes.DEFAULT_CONTENT_TYPE, new ByteArrayPart("p", new byte[0], null, null, null).getContentType()); + // An explicit content type is preserved and bypasses detection entirely. + assertEquals("application/custom", new ByteArrayPart("p", new byte[0], "application/custom", null, "x.png").getContentType()); + } +} diff --git a/pom.xml b/pom.xml index 65876824aa..ab826b3486 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,6 @@ 1.23.0 2.0.18 1.5.7-8 - 2.0.1 1.5.32 26.1.0 2.0.5 @@ -284,11 +283,6 @@ ${slf4j.version} - - com.sun.activation - jakarta.activation - ${activation.version} - org.jetbrains annotations