I feel so stupid now, but I was doing some testing for a SharePoint application that I’ve been working on and couldn’t work out why I was having trouble uploading test files to a document library.
This is a slightly long and pretty embarrassing tale of how I made a simple mistake that took a while to figure out. I’m reproducing the details hoping that you’ll find it useful in debugging similar SharePoint problems and to help you if you made a similarly silly mistake (unlikely!).
My application uses item-level auditing and I’ve got a custom page to show audit information for a bunch of documents in a library. I configured auditing and created my custom page. Then I created a test document in Windows Explorer (File ? New ? Text Document) and copied and pasted it a few times to create a series of documents I could upload to my document library.
My attempts at uploading any of these documents failed with an error “The file name is invalid or the file is empty. A file name cannot contain any of the following characters: \ / : * ? ” < > | # { } % ~ &”. WTF? My filename is “C:\Temp\Test Document1.txt”. It seems to have the right characters. I tried some of the other files, all failed. What’s going on???
Naturally I start wondering what I’ve done wrong. I’m working with a newly configured development environment – is that the problem? My solution is based on a custom list definition and custom content types and fields – did I mess something up there? Windows Update recently installed Internet Explorer 8 and the /_layouts/Upload.aspx page uses ActiveX – is there something wrong with my security configuration?
No. No. And, erm, NO! I’m going bald from the head scratching right now…
Debugging the problem
Hmmm… let’s try and see what the /_layouts/Upload.aspx page is doing. Opening the page in a text editor (location C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\Upload.aspx) we can see that it inherits from a class called UploadPage which is defined in the assembly Microsoft.SharePoint.ApplicationPages.
<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Page Language="C#" Inherits="Microsoft.SharePoint.ApplicationPages.UploadPage" ... %>
At this point I whip out every .NET developer’s best friend, Reflector, and start spelunking around at the UploadPage. class. Hmm… it’s a little hard to tell what’s going on. The OnLoad event is obfuscated, so there’s not much joy there.
OK, how about another approach. All SharePoint messages are stored in the resource files so maybe I can find out which resource key name is associated with my error message.
It turns out that the text “The file name is invalid or the file is empty” is defined in the file wss.resx located in (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\CONFIG\Resources) with a resource name of upload_document_file_invalid. A bit more searching around in Reflector and the 12 Hive files reveals that this resource is used in Upload.aspx.
We’re back to where we started but at least we have more information. The message is displayed by an ASP.NET CustomValidator control:
<asp:CustomValidator ControlToValidate="InputFile" Display = "Dynamic" ErrorMessage = "<%$Resources:wss,upload_document_file_invalid%>" OnServerValidate="ValidateFile" runat="server"/>
See the OnServerValidate attribute? That refers to a method in the super-class (or code-behind class if you prefer). Going back to Reflector we can see the definition for the ValidateFile method:
protected void ValidateFile(object source, ServerValidateEventArgs args) { CustomValidator validator = (CustomValidator) source; HtmlInputFile file = (HtmlInputFile) validator.FindControl(validator.ControlToValidate); if (SPUrlUtility.IsLegalFileName(this.GetLeafName(file.PostedFile.FileName))) { if ((file.PostedFile != null) && (file.PostedFile.ContentLength > 0)) { args.IsValid = true; return; } } else { args.IsValid = false; return; } args.IsValid = false; }
Now we’re getting somewhere! And, if you haven’t already worked out what I did wrong, you might have an idea of what’s wrong. It took me a little longer though!
I decided to implement my own class as a base class for Upload.aspx. That way I could interactively debug what’s going on while this code is executing. To do this I:
- Created a class in my solution that derives from Microsoft.SharePoint.ApplicationPages.UploadPage,
- Copied the ValidateFile method from the disassembled version in Reflector,
- Added the new modifier to the method declaration to override the non-virtual method in the base class,
- Searched in Reflector for and added definitions for other methods and types that ValidateFile depends on (but are marked as internal and therefore cannot be called directly. These included GetLeafName, IsLegalFileName, IsLegalCharInUrl and s_LegalUrlChars.
- Made a backup copy of Upload.aspx and modified the original Upload.aspx to inherit from my class *
My CustomUploadPage class file looked like:
using System; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using Microsoft.SharePoint.ApplicationPages; namespace Mratovich { public class CustomUploadPage : UploadPage { protected new void ValidateFile(object source, ServerValidateEventArgs args) { CustomValidator validator = (CustomValidator) source; HtmlInputFile file = (HtmlInputFile) validator.FindControl(validator.ControlToValidate); if (IsLegalFileName(this.GetLeafName(file.PostedFile.FileName))) { if ((file.PostedFile != null) && (file.PostedFile.ContentLength > 0)) { args.IsValid = true; return; } } else { args.IsValid = false; return; } args.IsValid = false; } private string GetLeafName(string s) { int num = s.LastIndexOf('\\'); if (num < 0) { return s; } return s.Substring(num + 1); } internal static bool IsLegalFileName(string name) { int num = -1; if (name == null) { throw new ArgumentNullException("name"); } if (name.Length == 0) { return false; } int num2 = 0; while (true) { if (num2 < name.Length) { if (!IsLegalCharInUrl(name[num2])) { return false; } if (name[num2] == '.') { if ((num2 == 0) || (num2 == (name.Length - 1))) { return false; } if ((num != -1) && (num == (num2 - 1))) { return false; } num = num2; } } else { return true; } num2++; } } internal static bool IsLegalCharInUrl(char ch) { if (ch >= '\x00a0') { return true; } while (ch == '/') { return false; } return s_LegalUrlChars[ch]; } private static bool[] s_LegalUrlChars = new bool[] { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false, false, true, false, false, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, false, true, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false }; } }
And here are the first few lines of my modified Upload.aspx file. I only added a reference to my custom assembly containing CustomUploadPage and the Page directive that indicates that the page inherits from my custom class. The rest is ommitted for brevity.
<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%> <%@ Assembly Name="Mratovich, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cba245c263929c41"%> <%@ Page Language="C#" Inherits="Mratovich.CustomUploadPage" MasterPageFile="~/_layouts/application.master" %> <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> ...
Now I could deploy my solution and place a breakpoint on my CustomUploadPage class’s ValidateFile method. When debugging, the name of the file turned out to be valid and the HtmlInputFile instance was created as expected. So where was this breaking down? Well in CustomUploadPage, the expression (file.PostedFile.ContentLength > 0) was evaluating to false.
Sure enough, when I double-checked the HTTP request in Fiddler the content for the file was empty!
Content-Disposition: form-data; name="ctl00$PlaceHolderMain$ctl01$ctl02$InputFile"; filename="C:\Temp\Test Document (3).txt" Content-Type: text/plain -----------------------------7d922d34101ac
Feeling pretty stupid right now…
At this point I slapped my forehead and nearly started to cry! How stupid! The file I was uploading was empty! The browser couldn’t send any content to the server because there was nothing to send! Remember the test files I created? Well look a little more carefully at the Size column!
0 KB! As soon as I entered some text into one of the files, saved it and tried to upload it, it worked!
Man I’m an idiot! 🙂 I hope you haven’t made as dumb a mistake as this. Hopefully I’ve passed on some tips on how to go about debugging this in SharePoint and using some tools that should be essential to your development arsenal. If nothing else you can have a laugh at my expense!
– Dario
* This isn’t a best practice of course. Modifying files in /_layouts is a bad idea, but this was just for debugging and I restored the backup of the original once I was finished.