New and Improved 3.13 Beta. Highlights: Shareable filters and dashboards and lots of other goodies. Any feedback can be raised as JIRA issues in the JIRA project.
Issue Details (XML | Word | Printable)

Key: JRA-11693
Type: Bug Bug
Status: Verified Verified
Priority: Critical Critical
Assignee: Unassigned
Reporter: Ralf Beckers
Votes: 30
Watchers: 25
Available Workflow Actions

Start Progress
Operations

If you were logged in you would be able to see more operations.
JIRA

SOAP: addAttachmentsToIssue runs out of memory when adding attachments of 1.5 MB or larger

Created: 06/Dec/06 05:34 AM   Updated: 23/Jul/08 02:49 AM
Component/s: Remote API (SOAP & XML-RPC)
Affects Version/s: 3.6.4
Fix Version/s: None

Time Tracking:
Not Specified

File Attachments: 1. Text File addAttachment.txt (755 kB)
2. HTML File largeobjects.html (5 kB)

Issue Links:
Duplicate
 
Reference
 

Participants: Anton Mazkovoi [Atlassian], Antti Lehto, Chris Mountford [Atlassian], Daniel Weber, Darren Martz, Dylan Etkin [Atlassian], Eugene Koutsenko, Jeff Turner [Atlassian], Matt Doar, Peter Burkholder, Ralf Beckers, Sascha Weinreuter, Stefan Hausmann, Vladimir Dyuzhev and Xavier Guilbeault
Since last comment: 5 weeks, 1 day ago
To be done by: Pair of developers
Support reference count: 6
Labels:


 Description  « Hide
Calling addAttachmentsToIssue it works for small data, but for large data like a picture with 1.5 MB I got an error:
java.lang.OutOfMemoryError; nested exception is: java.lang.OutOfMemoryError

http://forums.atlassian.com/thread.jspa?threadID=14442&tstart=0
http://forums.atlassian.com/thread.jspa?messageID=257234488



 All   Comments   Work Log   Change History      Sort Order: Ascending order - Click to sort in descending order
Chris Mountford [Atlassian] added a comment - 20/Dec/06 01:13 AM
The problem appears to be that axis 1.3 in its wisdom uses a jurassic amount of RAM compared to the attachment size. The OOME on the client happens probably because the overhead is too much of storing a DOM containing many elements and attributes to describe every single byte in the attachment.

Axis 2 purports to feature a low memory overhead so this may be the way forward for jira-soapclient.


Sascha Weinreuter added a comment - 29/Dec/06 11:42 AM
Is there any workaround for the memory (and performance!) problem? I tried to create clients with XFire and Axis2, but both of them failed with strange errors when trying to generate the client code. Shouldn't the WebService just work with any WS-Toolkit?

Chris Mountford [Atlassian] added a comment - 31/Jan/07 02:00 AM
note that this problem exists with the jira-soapclient sample code, not with JIRA itself.

Xavier Guilbeault added a comment - 04/May/07 06:35 AM
As a note, the only way we could finally have a working solution is by doing "Page scraping" and by building a HTML POST message containing the attachment. It is not very elegant, nor practical, but this way we were able to upload any file with the same efficiency as with the web interface.

Ralf Beckers added a comment - 05/Jun/07 03:15 AM
Hello Xavier,

thanx for the info. Can you tell me how to build an URL for doing so,
or do you have some example code for me?


Jeff Turner [Atlassian] added a comment - 11/Jul/07 08:27 PM
This is definitely a server-side problem that does affect SOAPpy:
>>> soap.addAttachmentsToIssue(auth, "TP-1", ["largefile.txt"], [[file_contents]])
<Fault soapenv:Server.generalException: java.lang.OutOfMemoryError: Java heap space; nested exception is: 
        java.lang.OutOfMemoryError: Java heap space: <SOAPpy.Types.structType detail at 146593164>: {'hostname': 'psyche', 'faultData': ''}>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/var/lib/python-support/python2.5/SOAPpy/Client.py", line 421, in __call__
    return self.__r_call(*args, **kw)
  File "/var/lib/python-support/python2.5/SOAPpy/Client.py", line 443, in __r_call
    self.__hd, self.__ma)
  File "/var/lib/python-support/python2.5/SOAPpy/Client.py", line 357, in __call
    raise p
SOAPpy.Types.faultType: <Fault soapenv:Server.generalException: java.lang.OutOfMemoryError: Java heap space; nested exception is: 
        java.lang.OutOfMemoryError: Java heap space: <SOAPpy.Types.structType detail at 146593164>: {'hostname': 'psyche', 'faultData': ''}>
>>> 

By setting -XX:+HeapDumpOnOutOfMemoryError I can get a memory dump when JIRA OOMs, and it's all Axis classes using the memory


Jeff Turner [Atlassian] added a comment - 11/Jul/07 09:19 PM - edited
Until we get this fixed, a workaround is to use plain HTTP to upload files. Eg:

$ curl -s 'http://localhost:8080/secure/AttachFile.jspa?id=10010&os_username=test&os_password=test' -F filename.1=@largefile.txt > /dev/null


Xavier Guilbeault added a comment - 20/Jul/07 06:30 AM
Hi Ralf, here is the function, in c#, doing the upload with a HTTP POST, its probably not perfect, but it does the job :

private bool UploadFileWithPost( string filename, string issueID )
{
try
{
string loginUrl = "http://localhost:8080/secure/AttachFile.jspa"+ "?id=" + issueID + "&os_username=" + loginName + "&os_password=" + password;

// Create the bytes array for the file to upload.
FileInfo fileInfo = new FileInfo(filename);
if (!fileInfo.Exists)

{ return false; }

HttpWebRequest webRequestRFC = (HttpWebRequest)WebRequest.Create(loginUrl);
webRequestRFC.CookieContainer = new CookieContainer();

// Auth with server, and fetch cookie for session ID
System.IO.StreamReader responseReader = new System.IO.StreamReader(webRequestRFC.GetResponse().GetResponseStream());
string responseData = responseReader.ReadToEnd();
responseReader.Close();

// Create 2nd request
HttpWebRequest webRequestRFC2 = (HttpWebRequest)WebRequest.Create("http://localhost:8080/secure/AttachFile.jspa");
// Copy cookie
webRequestRFC2.CookieContainer = webRequestRFC.CookieContainer;

// Generate random boundary, should really be random rather than a fixed string like this
string boundary = "----------AaB03x-";

string stringDataRFC =
boundary + "\n" +
"Content-disposition: form-data; name=\"multiple\"\n\n" +
"false\n" +
boundary + "\n" +
"Content-Disposition: form-data; name=\"id\"\n\n" +
issueID + "\n" +
boundary + "\n" +
"Content-Disposition: form-data; name=\"Attach\"\n\n" +
"Attach\n" +
boundary + "\n";

stringDataRFC += "Content-Disposition: form-data; name=\"filename.1\"; filename=\"" + filename + "\"\n" +
"Content-Type: application/octet-stream\n\n";

string strEndTag = "\n" + boundary + "\n";
byte[] dataEndTag = Encoding.UTF8.GetBytes(strEndTag);

byte[] dataRFCbytes = Encoding.UTF8.GetBytes(stringDataRFC);

webRequestRFC2.Method = "POST";
webRequestRFC2.Accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, /";
webRequestRFC2.ContentType = "multipart/form-data; boundary=" + boundary;
webRequestRFC2.ContentLength = dataRFCbytes.Length + dataEndTag.Length + fileInfo.Length;
System.IO.Stream newStream = webRequestRFC2.GetRequestStream();
newStream.Write(dataRFCbytes, 0, dataRFCbytes.Length);

// Copy file
System.IO.FileStream fileStream = new System.IO.FileStream(filename, System.IO.FileMode.Open);
BinaryReader binFileReader = new BinaryReader(fileStream);
System.IO.BinaryWriter binWriter = new System.IO.BinaryWriter(newStream);

int bufSize = 1024;
byte[] buf = new byte[bufSize];

int read = 0;
int total = 0;
while ((read = binFileReader.Read(buf, 0, bufSize)) != 0)

{ total += read; binWriter.Write(buf, 0, read); }

// Copy end of tag
newStream.Write(dataEndTag, 0, dataEndTag.Length);
newStream.Close();
binWriter.Close();
binFileReader.Close();
fileStream.Close();

return true;

}
catch (Exception ex)

{ return false; }

}


Vladimir Dyuzhev added a comment - 08/Aug/07 12:30 AM
It (=Axis encoding of every byte as a node) probably explains an extremely long upload time. In test run a 100K file takes 10 min+ to get attached to TST project on Atlassian site. (( And the network is constantly busy all this time, which suggests a VERY BIG xml.

Vladimir Dyuzhev added a comment - 08/Aug/07 10:36 AM
Oh, my! To send a small file of 3K Axis generates 700K SOAP message!

An excerpt (full version of the message see in the attachment):

<in3 soapenc:arrayType="xsd:byte[][1]" xsi:type="soapenc:Array" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<in3 soapenc:arrayType="xsd:byte[3392]" xsi:type="soapenc:Array">
<in3 href="#id0"/>
<in3 href="#id1"/>
<in3 href="#id2"/>
...
<in3 href="#id3389"/>
<in3 href="#id3390"/>
<in3 href="#id3391"/>
</in3>
</in3>
</ns1:addAttachmentsToIssue>
<multiRef id="id884" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">89</multiRef>
<multiRef id="id2428" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">89</multiRef>
<multiRef id="id201" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">87</multiRef>
<multiRef id="id1507" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">112</multiRef>
...
<multiRef id="id3309" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">72</multiRef>
<multiRef id="id1166" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:byte" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">82</multiRef>
</soapenv:Body>
</soapenv:Envelope>

CRAZY!!! ((((


Vladimir Dyuzhev added a comment - 08/Aug/07 10:37 AM
P.S. Axis 1.4

This API is essentially unusable for anything except small text files.


Antti Lehto added a comment - 09/Aug/07 05:12 AM
Got the same problem in Jira 3.8.1.

Shouldn't the filecontent be defined as xsd:base64binary instead of byte array in wsdl?


Vladimir Dyuzhev added a comment - 09/Aug/07 06:43 PM
For those who need to do it from Java: a quick'n'dirty addAttachmentToIssue method using Apache HttpClient 3.0.1:

Work both via proxy and directly. Interesting that I had to disable transfer-encoding, otherwise JIRA corrupts attaches.
Adjust to your taste.

=====
package td2jira.jira.http;

import java.io.File;
import java.io.FileOutputStream;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.log4j.Logger;

import td2jira.Config;
import td2jira.jira.JIRAIssue;

public class AddAttachmentToIssue {
private static Logger logger = Logger.getLogger(AddAttachmentToIssue.class);

public static void addAttachmentToIssue(JIRAIssue jiraIssue,String fileName, byte[] data) {
try { logger.debug("adding attachment "+fileName+" to "+jiraIssue.getKey()+"..."); HttpClient httpclient = new HttpClient(); auth(httpclient); doLogin(httpclient); doAttach(httpclient, jiraIssue.getId(), fileName, data); } catch( Exception ex ) { throw new RuntimeException(ex); }
}

private static void auth(HttpClient httpclient) {
if( Config.HTTP_PROXY_HOST != null ) {
logger.debug("using proxy "Config.HTTP_PROXY_HOST":"+Config.HTTP_PROXY_PORT);

HostConfiguration hc = httpclient.getHostConfiguration();
hc.setProxy(Config.HTTP_PROXY_HOST,Integer.parseInt(Config.HTTP_PROXY_PORT));

if( Config.HTTP_PROXY_USER != null ) {
logger.debug("using proxy user "+Config.HTTP_PROXY_USER);
Credentials proxyCredentials = null;

int backSlash = Config.HTTP_PROXY_USER.indexOf('
');
if( backSlash > 0 ) { String domain = Config.HTTP_PROXY_USER.substring(0, backSlash); String user = Config.HTTP_PROXY_USER.substring(backSlash+1); String computer = "CBAD4-KCILR1"; logger.debug("using NT proxy authorization: "+user+"@"+domain+" on "+computer); proxyCredentials = new NTCredentials(user,Config.HTTP_PROXY_PASSWORD,computer,domain); } else { proxyCredentials = new UsernamePasswordCredentials(Config.HTTP_PROXY_USER, Config.HTTP_PROXY_PASSWORD); }
httpclient.getState().setCredentials(AuthScope.ANY, proxyCredentials);
}
}
}

private static void doLogin(HttpClient httpclient) throws Exception {
String loginUrl = Config.JIRA_URL+"/login.jsp";
PostMethod login = new PostMethod(loginUrl);
login.addParameter("os_username", Config.JIRA_USER);
login.addParameter("os_password", Config.JIRA_PASSWORD);
login.addParameter("os_destination", "/secure");

int code = httpclient.executeMethod(login);
String html = login.getResponseBodyAsString();
if( html.indexOf("os_username") >= 0 ) { System.out.println("code:"+code); System.out.println(html); throw new RuntimeException("failed to login as "+Config.JIRA_USER); }
}

private static void doAttach(HttpClient httpclient,String issueId,String fileName,byte[] data) throws Exception {
File tmp = new File(fileName);
tmp.deleteOnExit();

FileOutputStream fos = new FileOutputStream(tmp);
fos.write(data);
fos.close();

String attachUrl = Config.JIRA_URL+"/secure/AttachFile.jspa?id="+issueId;

PostMethod attach = new PostMethod(attachUrl);

Part[] parts = { new StringPart("multiple", "false"), new StringPart("id", issueId), new StringPart("Attach", "Attach"), new StringPart("comment", Config.SYNC_ATTACH_COMMENT_PREFIX+fileName), new StringPart("commentLevel", ""), new FilePart("filename.1", tmp) };

// NB: gotta do it, otherwise JIRA corrupts attachments and mangles comments!!!
for (Part part : parts) {
if( part instanceof FilePart ) { FilePart fp = (FilePart) part; fp.setTransferEncoding(null); } else if( part instanceof StringPart ) { StringPart sp = (StringPart) part; sp.setTransferEncoding(null); }
}

attach.setRequestEntity(new MultipartRequestEntity(parts, attach.getParams()));

httpclient.executeMethod(attach);
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
// attach.getRequestEntity().writeRequest(baos);
// baos.close();
// fos = new FileOutputStream("req.txt");
// fos.write(baos.toByteArray());
// fos.close();
}
}

=====


Matt Doar added a comment - 20/Dec/07 01:36 PM
Am I understanding Jeff's comment correctly that any of my users can crash their JIRA instance using this SOAP method? That's quite a denial of service problem.

Daniel Weber added a comment - 08/Jan/08 05:43 AM
When did u get the fix for this problem? Please write a date

Matt Doar added a comment - 08/Jan/08 10:46 AM
I don't see a fix.

Stefan Hausmann added a comment - 18/Jun/08 11:36 AM
I think the FileTransportFormat is already Base64:
<wsdl:message name="addAttachmentsToIssueRequest">
<wsdl:part name="in0" type="xsd:string"/>
<wsdl:part name="in1" type="xsd:string"/>
<wsdl:part name="in2" type="impl:ArrayOf_xsd_string"/>
<wsdl:part name="in3" type="impl:ArrayOf_xsd_base64Binary"/>
</wsdl:message>

But perhaps the FileByteArray as a normal invoke parameter is not a good idea!
The following Axis classes also have a special API for attachments:
org.apache.axis.client.Call
org.apache.axis.client.Stub

Sorry, I'm not very involved in the SOAP area.
But this might be a hint for an expert.


Peter Burkholder added a comment - 30/Jun/08 03:49 PM
Here's the key parts of upload attachments via Post in Ruby:
Unable to find source-code formatter for language: ruby. Available languages are: javascript, sql, xhtml, actionscript, none, html, xml, java
#!/bin/ruby

require 'httpclient'
require 'mime/types'
require 'cgi'

password= IO.read('./password').chomp
password= CGI::escape(password)
username= "username"

jira    = "http://jira.example.com:8080"
id      = "22741"  # You'll need to fetch this with the SOAP client for an issue key
url     = jira + '/secure/AttachFile.jspa?id=' + id + '&os_username=' + username + '&os_password=' + password

# From http://wiki.rubyonrails.org/rails/pages/HowToSendAFileByPostMultipart
# with updates for httpclient2

file1 = File.open('image.gif')
form = { 'filename.1' => file1, 'comment' => "Some Comment" }

boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
extheader = {'content-type' => "multipart/form-data; boundary=___#{ boundary }___"}

client = HTTPClient.new

response = client.post_content(url, form, extheader)

Eugene Koutsenko added a comment - 21/Jul/08 08:41 PM
Hi Guys,

There is a better way to fix this than HTTP request. I have the issue that in our system we use LDAP SSO integration - so Vladimir's fix doesn't work - I can't log on to LDAP SSO reliably with HTTP Request - It keeps telling me my ticket tokens are incorrect etc.

So, i've been trying to work out how to fix this for the last few days - and the simplest answer is to write a small fix in the plugin, and to use the method jiraSoapService.addAttachmentsToIssue almost as intended.

The Fix is as follows:

  • The issue is that AXIS has problems encoding binary data (byte arrays), and that they should be in Base64 as pointed out in the comments above. So - why don't we encode the byte arrays into Base64 for it?... Using even the same AXIS library...
  • The jiraSoapService.addAttachmentsToIssue method accepts a String[] fileNames, and a byte[][] attachments. I propose ignore the byte[][] - just pass a new byte[1][] - empty array in there instead.
  • Use the String[] filenames to pass all the data - the actual filenames, along with the byte arrays encoded as Base64 strings...
  • The logic is: Every odd member of the String[] fileNames is the actual filename, and every even member is the Base64 String representing the data...

The code for the client side is as follows... It assumes that you have a String[] strNames, and a byte[][] bteBinaries ready to go for submitting to the addAttachmentsToIssue method.

----------------------------------

//Workaround for AXIS - Eugene Koutsenko 22/07/08

String[] tmpFiles = new String[strNames.length*2];

for (i=0; i < strNames.length; i++)

{ tmpFiles[i*2] = strNames[i]; tmpFiles[i*2+1]= org.apache.axis.encoding.Base64.encode(bteBinaries[i]); }

strNames = tmpFiles;
bteBinaries = new byte[1][];

//End Workaround

jiraSoapService.addAttachmentsToIssue(authToken,returnedIssue.getKey(),strNames,bteBinaries);

----------------------------------

And at the server side, you will have to modify IssueServiceImpl.java inside atlassian-jira-rpc-plugin-xxx.jar - it's under services. The mod is in the addAttachmentsToIssue method - basically we have to decode the information sent by the code above, and put it back into the familiar format expected by the RPC plugin - and then let it do its thing.

----------------------------------

//Fix for AXIS issue with passing binaries (http://jira.atlassian.com/browse/JRA-11693). Instead, pass the binary as a Base 64
//encoded string within AXIS, and recompile back into what was expected on the server.
//- Eugene Koutsenko 22/07/08

if (fileNames.length % 2 != 0) throw new RemoteValidationException("Number of items in fileNames is not even. Expected: Names in odd members, Base64 byte arrays in even.");

String[] tmpFileNames = new String[fileNames.length / 2];
byte[][] tmpAttachments = new byte[fileNames.length / 2][];

for (int i = 1; i <= fileNames.length / 2; i++)

{ tmpFileNames[i-1] = fileNames[i*2-2]; tmpAttachments[i-1] = org.apache.axis.encoding.Base64.decode(fileNames[i*2-1]); }

fileNames = tmpFileNames;
attachments = tmpAttachments;

//End Fix

//... Continue on with the rest of method...

---------------------------------

That's it. Just tested it out locally - works like a charm.

Hope it helps!

Eugene.


Darren Martz added a comment - 23/Jul/08 02:49 AM
Nice patch Eugene

Would you mind uploading a patched JAR file for 3.12.3 for those of us who have yet to configure a jira build environment?

The C# version of your patch is as follows (untested but it should just work)...

private void AddAttachmentsToIssue(string issuekey, string[] filenames)
        {
            //Workaround for AXIS - Eugene Koutsenko 22/07/08

            string[] tmpfiles = new string[filenames.Length * 2];
            byte[] data;         

            for (int i = 0; i < filenames.Length; i++)
            {
                tmpfiles[i * 2] = System.IO.Path.GetFileName(filenames[i]);
                data = File.ReadAllBytes(filenames[i]);
                tmpfiles[i * 2 + 1] = Convert.ToBase64String(data, Base64FormattingOptions.InsertLineBreaks);                                        
            }

            filenames = tmpfiles;            

            //End Workaround

            _jss.addAttachmentsToIssue(_token, issuekey, filenames, new sbyte[1] { 0 });
        }

It seems rather memory heavy given its not chunked, but it looks like it should work.