View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.multipart;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpContent;
21  import io.netty.handler.codec.http.HttpRequest;
22  import io.netty.handler.codec.http.LastHttpContent;
23  import io.netty.handler.codec.http.QueryStringDecoder;
24  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException;
25  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
26  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
27  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
28  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
29  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
30  
31  import java.io.IOException;
32  import java.nio.charset.Charset;
33  import java.util.ArrayList;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.TreeMap;
37  
38  import static io.netty.buffer.Unpooled.*;
39  
40  /**
41   * This decoder will decode Body and can handle POST BODY.
42   *
43   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
44   *
45   */
46  public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestDecoder {
47  
48      /**
49       * Factory used to create InterfaceHttpData
50       */
51      private final HttpDataFactory factory;
52  
53      /**
54       * Request to decode
55       */
56      private final HttpRequest request;
57  
58      /**
59       * Default charset to use
60       */
61      private final Charset charset;
62  
63      /**
64       * Does the last chunk already received
65       */
66      private boolean isLastChunk;
67  
68      /**
69       * HttpDatas from Body
70       */
71      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
72  
73      /**
74       * HttpDatas as Map from Body
75       */
76      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
77              CaseIgnoringComparator.INSTANCE);
78  
79      /**
80       * The current channelBuffer
81       */
82      private ByteBuf undecodedChunk;
83  
84      /**
85       * Body HttpDatas current position
86       */
87      private int bodyListHttpDataRank;
88  
89      /**
90       * Current getStatus
91       */
92      private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
93  
94      /**
95       * The current Attribute that is currently in decode process
96       */
97      private Attribute currentAttribute;
98  
99      private boolean destroyed;
100 
101     private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
102 
103     /**
104      *
105      * @param request
106      *            the request to decode
107      * @throws NullPointerException
108      *             for request
109      * @throws ErrorDataDecoderException
110      *             if the default charset was wrong when decoding or other
111      *             errors
112      */
113     public HttpPostStandardRequestDecoder(HttpRequest request) {
114         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
115     }
116 
117     /**
118      *
119      * @param factory
120      *            the factory used to create InterfaceHttpData
121      * @param request
122      *            the request to decode
123      * @throws NullPointerException
124      *             for request or factory
125      * @throws ErrorDataDecoderException
126      *             if the default charset was wrong when decoding or other
127      *             errors
128      */
129     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) {
130         this(factory, request, HttpConstants.DEFAULT_CHARSET);
131     }
132 
133     /**
134      *
135      * @param factory
136      *            the factory used to create InterfaceHttpData
137      * @param request
138      *            the request to decode
139      * @param charset
140      *            the charset to use as default
141      * @throws NullPointerException
142      *             for request or charset or factory
143      * @throws ErrorDataDecoderException
144      *             if the default charset was wrong when decoding or other
145      *             errors
146      */
147     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
148         if (factory == null) {
149             throw new NullPointerException("factory");
150         }
151         if (request == null) {
152             throw new NullPointerException("request");
153         }
154         if (charset == null) {
155             throw new NullPointerException("charset");
156         }
157         this.request = request;
158         this.charset = charset;
159         this.factory = factory;
160         if (request instanceof HttpContent) {
161             // Offer automatically if the given request is als type of HttpContent
162             // See #1089
163             offer((HttpContent) request);
164         } else {
165             undecodedChunk = buffer();
166             parseBody();
167         }
168     }
169 
170     private void checkDestroyed() {
171         if (destroyed) {
172             throw new IllegalStateException(HttpPostStandardRequestDecoder.class.getSimpleName()
173                     + " was destroyed already");
174         }
175     }
176 
177     /**
178      * True if this request is a Multipart request
179      *
180      * @return True if this request is a Multipart request
181      */
182     @Override
183     public boolean isMultipart() {
184         checkDestroyed();
185         return false;
186     }
187 
188     /**
189      * Set the amount of bytes after which read bytes in the buffer should be discarded.
190      * Setting this lower gives lower memory usage but with the overhead of more memory copies.
191      * Use {@code 0} to disable it.
192      */
193     @Override
194     public void setDiscardThreshold(int discardThreshold) {
195         if (discardThreshold < 0) {
196           throw new IllegalArgumentException("discardThreshold must be >= 0");
197         }
198         this.discardThreshold = discardThreshold;
199     }
200 
201     /**
202      * Return the threshold in bytes after which read data in the buffer should be discarded.
203      */
204     @Override
205     public int getDiscardThreshold() {
206         return discardThreshold;
207     }
208 
209     /**
210      * This getMethod returns a List of all HttpDatas from body.<br>
211      *
212      * If chunked, all chunks must have been offered using offer() getMethod. If
213      * not, NotEnoughDataDecoderException will be raised.
214      *
215      * @return the list of HttpDatas from Body part for POST getMethod
216      * @throws NotEnoughDataDecoderException
217      *             Need more chunks
218      */
219     @Override
220     public List<InterfaceHttpData> getBodyHttpDatas() {
221         checkDestroyed();
222 
223         if (!isLastChunk) {
224             throw new NotEnoughDataDecoderException();
225         }
226         return bodyListHttpData;
227     }
228 
229     /**
230      * This getMethod returns a List of all HttpDatas with the given name from
231      * body.<br>
232      *
233      * If chunked, all chunks must have been offered using offer() getMethod. If
234      * not, NotEnoughDataDecoderException will be raised.
235      *
236      * @return All Body HttpDatas with the given name (ignore case)
237      * @throws NotEnoughDataDecoderException
238      *             need more chunks
239      */
240     @Override
241     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
242         checkDestroyed();
243 
244         if (!isLastChunk) {
245             throw new NotEnoughDataDecoderException();
246         }
247         return bodyMapHttpData.get(name);
248     }
249 
250     /**
251      * This getMethod returns the first InterfaceHttpData with the given name from
252      * body.<br>
253      *
254      * If chunked, all chunks must have been offered using offer() getMethod. If
255      * not, NotEnoughDataDecoderException will be raised.
256      *
257      * @return The first Body InterfaceHttpData with the given name (ignore
258      *         case)
259      * @throws NotEnoughDataDecoderException
260      *             need more chunks
261      */
262     @Override
263     public InterfaceHttpData getBodyHttpData(String name) {
264         checkDestroyed();
265 
266         if (!isLastChunk) {
267             throw new NotEnoughDataDecoderException();
268         }
269         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
270         if (list != null) {
271             return list.get(0);
272         }
273         return null;
274     }
275 
276     /**
277      * Initialized the internals from a new chunk
278      *
279      * @param content
280      *            the new received chunk
281      * @throws ErrorDataDecoderException
282      *             if there is a problem with the charset decoding or other
283      *             errors
284      */
285     @Override
286     public HttpPostStandardRequestDecoder offer(HttpContent content) {
287         checkDestroyed();
288 
289         // Maybe we should better not copy here for performance reasons but this will need
290         // more care by the caller to release the content in a correct manner later
291         // So maybe something to optimize on a later stage
292         ByteBuf buf = content.content();
293         if (undecodedChunk == null) {
294             undecodedChunk = buf.copy();
295         } else {
296             undecodedChunk.writeBytes(buf);
297         }
298         if (content instanceof LastHttpContent) {
299             isLastChunk = true;
300         }
301         parseBody();
302         if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
303             undecodedChunk.discardReadBytes();
304         }
305         return this;
306     }
307 
308     /**
309      * True if at current getStatus, there is an available decoded
310      * InterfaceHttpData from the Body.
311      *
312      * This getMethod works for chunked and not chunked request.
313      *
314      * @return True if at current getStatus, there is a decoded InterfaceHttpData
315      * @throws EndOfDataDecoderException
316      *             No more data will be available
317      */
318     @Override
319     public boolean hasNext() {
320         checkDestroyed();
321 
322         if (currentStatus == MultiPartStatus.EPILOGUE) {
323             // OK except if end of list
324             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
325                 throw new EndOfDataDecoderException();
326             }
327         }
328         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
329     }
330 
331     /**
332      * Returns the next available InterfaceHttpData or null if, at the time it
333      * is called, there is no more available InterfaceHttpData. A subsequent
334      * call to offer(httpChunk) could enable more data.
335      *
336      * Be sure to call {@link InterfaceHttpData#release()} after you are done
337      * with processing to make sure to not leak any resources
338      *
339      * @return the next available InterfaceHttpData or null if none
340      * @throws EndOfDataDecoderException
341      *             No more data will be available
342      */
343     @Override
344     public InterfaceHttpData next() {
345         checkDestroyed();
346 
347         if (hasNext()) {
348             return bodyListHttpData.get(bodyListHttpDataRank++);
349         }
350         return null;
351     }
352 
353     /**
354      * This getMethod will parse as much as possible data and fill the list and map
355      *
356      * @throws ErrorDataDecoderException
357      *             if there is a problem with the charset decoding or other
358      *             errors
359      */
360     private void parseBody() {
361         if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
362             if (isLastChunk) {
363                 currentStatus = MultiPartStatus.EPILOGUE;
364             }
365             return;
366         }
367         parseBodyAttributes();
368     }
369 
370     /**
371      * Utility function to add a new decoded data
372      */
373     protected void addHttpData(InterfaceHttpData data) {
374         if (data == null) {
375             return;
376         }
377         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
378         if (datas == null) {
379             datas = new ArrayList<InterfaceHttpData>(1);
380             bodyMapHttpData.put(data.getName(), datas);
381         }
382         datas.add(data);
383         bodyListHttpData.add(data);
384     }
385 
386     /**
387      * This getMethod fill the map and list with as much Attribute as possible from
388      * Body in not Multipart mode.
389      *
390      * @throws ErrorDataDecoderException
391      *             if there is a problem with the charset decoding or other
392      *             errors
393      */
394     private void parseBodyAttributesStandard() {
395         int firstpos = undecodedChunk.readerIndex();
396         int currentpos = firstpos;
397         int equalpos;
398         int ampersandpos;
399         if (currentStatus == MultiPartStatus.NOTSTARTED) {
400             currentStatus = MultiPartStatus.DISPOSITION;
401         }
402         boolean contRead = true;
403         try {
404             while (undecodedChunk.isReadable() && contRead) {
405                 char read = (char) undecodedChunk.readUnsignedByte();
406                 currentpos++;
407                 switch (currentStatus) {
408                 case DISPOSITION:// search '='
409                     if (read == '=') {
410                         currentStatus = MultiPartStatus.FIELD;
411                         equalpos = currentpos - 1;
412                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
413                                 charset);
414                         currentAttribute = factory.createAttribute(request, key);
415                         firstpos = currentpos;
416                     } else if (read == '&') { // special empty FIELD
417                         currentStatus = MultiPartStatus.DISPOSITION;
418                         ampersandpos = currentpos - 1;
419                         String key = decodeAttribute(
420                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
421                         currentAttribute = factory.createAttribute(request, key);
422                         currentAttribute.setValue(""); // empty
423                         addHttpData(currentAttribute);
424                         currentAttribute = null;
425                         firstpos = currentpos;
426                         contRead = true;
427                     }
428                     break;
429                 case FIELD:// search '&' or end of line
430                     if (read == '&') {
431                         currentStatus = MultiPartStatus.DISPOSITION;
432                         ampersandpos = currentpos - 1;
433                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
434                         firstpos = currentpos;
435                         contRead = true;
436                     } else if (read == HttpConstants.CR) {
437                         if (undecodedChunk.isReadable()) {
438                             read = (char) undecodedChunk.readUnsignedByte();
439                             currentpos++;
440                             if (read == HttpConstants.LF) {
441                                 currentStatus = MultiPartStatus.PREEPILOGUE;
442                                 ampersandpos = currentpos - 2;
443                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
444                                 firstpos = currentpos;
445                                 contRead = false;
446                             } else {
447                                 // Error
448                                 throw new ErrorDataDecoderException("Bad end of line");
449                             }
450                         } else {
451                             currentpos--;
452                         }
453                     } else if (read == HttpConstants.LF) {
454                         currentStatus = MultiPartStatus.PREEPILOGUE;
455                         ampersandpos = currentpos - 1;
456                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
457                         firstpos = currentpos;
458                         contRead = false;
459                     }
460                     break;
461                 default:
462                     // just stop
463                     contRead = false;
464                 }
465             }
466             if (isLastChunk && currentAttribute != null) {
467                 // special case
468                 ampersandpos = currentpos;
469                 if (ampersandpos > firstpos) {
470                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
471                 } else if (!currentAttribute.isCompleted()) {
472                     setFinalBuffer(EMPTY_BUFFER);
473                 }
474                 firstpos = currentpos;
475                 currentStatus = MultiPartStatus.EPILOGUE;
476                 undecodedChunk.readerIndex(firstpos);
477                 return;
478             }
479             if (contRead && currentAttribute != null) {
480                 // reset index except if to continue in case of FIELD getStatus
481                 if (currentStatus == MultiPartStatus.FIELD) {
482                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
483                                                 false);
484                     firstpos = currentpos;
485                 }
486                 undecodedChunk.readerIndex(firstpos);
487             } else {
488                 // end of line or end of block so keep index to last valid position
489                 undecodedChunk.readerIndex(firstpos);
490             }
491         } catch (ErrorDataDecoderException e) {
492             // error while decoding
493             undecodedChunk.readerIndex(firstpos);
494             throw e;
495         } catch (IOException e) {
496             // error while decoding
497             undecodedChunk.readerIndex(firstpos);
498             throw new ErrorDataDecoderException(e);
499         }
500     }
501 
502     /**
503      * This getMethod fill the map and list with as much Attribute as possible from
504      * Body in not Multipart mode.
505      *
506      * @throws ErrorDataDecoderException
507      *             if there is a problem with the charset decoding or other
508      *             errors
509      */
510     private void parseBodyAttributes() {
511         SeekAheadOptimize sao;
512         try {
513             sao = new SeekAheadOptimize(undecodedChunk);
514         } catch (SeekAheadNoBackArrayException ignored) {
515             parseBodyAttributesStandard();
516             return;
517         }
518         int firstpos = undecodedChunk.readerIndex();
519         int currentpos = firstpos;
520         int equalpos;
521         int ampersandpos;
522         if (currentStatus == MultiPartStatus.NOTSTARTED) {
523             currentStatus = MultiPartStatus.DISPOSITION;
524         }
525         boolean contRead = true;
526         try {
527             loop: while (sao.pos < sao.limit) {
528                 char read = (char) (sao.bytes[sao.pos++] & 0xFF);
529                 currentpos++;
530                 switch (currentStatus) {
531                 case DISPOSITION:// search '='
532                     if (read == '=') {
533                         currentStatus = MultiPartStatus.FIELD;
534                         equalpos = currentpos - 1;
535                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
536                                 charset);
537                         currentAttribute = factory.createAttribute(request, key);
538                         firstpos = currentpos;
539                     } else if (read == '&') { // special empty FIELD
540                         currentStatus = MultiPartStatus.DISPOSITION;
541                         ampersandpos = currentpos - 1;
542                         String key = decodeAttribute(
543                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
544                         currentAttribute = factory.createAttribute(request, key);
545                         currentAttribute.setValue(""); // empty
546                         addHttpData(currentAttribute);
547                         currentAttribute = null;
548                         firstpos = currentpos;
549                         contRead = true;
550                     }
551                     break;
552                 case FIELD:// search '&' or end of line
553                     if (read == '&') {
554                         currentStatus = MultiPartStatus.DISPOSITION;
555                         ampersandpos = currentpos - 1;
556                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
557                         firstpos = currentpos;
558                         contRead = true;
559                     } else if (read == HttpConstants.CR) {
560                         if (sao.pos < sao.limit) {
561                             read = (char) (sao.bytes[sao.pos++] & 0xFF);
562                             currentpos++;
563                             if (read == HttpConstants.LF) {
564                                 currentStatus = MultiPartStatus.PREEPILOGUE;
565                                 ampersandpos = currentpos - 2;
566                                 sao.setReadPosition(0);
567                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
568                                 firstpos = currentpos;
569                                 contRead = false;
570                                 break loop;
571                             } else {
572                                 // Error
573                                 sao.setReadPosition(0);
574                                 throw new ErrorDataDecoderException("Bad end of line");
575                             }
576                         } else {
577                             if (sao.limit > 0) {
578                                 currentpos--;
579                             }
580                         }
581                     } else if (read == HttpConstants.LF) {
582                         currentStatus = MultiPartStatus.PREEPILOGUE;
583                         ampersandpos = currentpos - 1;
584                         sao.setReadPosition(0);
585                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
586                         firstpos = currentpos;
587                         contRead = false;
588                         break loop;
589                     }
590                     break;
591                 default:
592                     // just stop
593                     sao.setReadPosition(0);
594                     contRead = false;
595                     break loop;
596                 }
597             }
598             if (isLastChunk && currentAttribute != null) {
599                 // special case
600                 ampersandpos = currentpos;
601                 if (ampersandpos > firstpos) {
602                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
603                 } else if (!currentAttribute.isCompleted()) {
604                     setFinalBuffer(EMPTY_BUFFER);
605                 }
606                 firstpos = currentpos;
607                 currentStatus = MultiPartStatus.EPILOGUE;
608                 undecodedChunk.readerIndex(firstpos);
609                 return;
610             }
611             if (contRead && currentAttribute != null) {
612                 // reset index except if to continue in case of FIELD getStatus
613                 if (currentStatus == MultiPartStatus.FIELD) {
614                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
615                                                 false);
616                     firstpos = currentpos;
617                 }
618                 undecodedChunk.readerIndex(firstpos);
619             } else {
620                 // end of line or end of block so keep index to last valid position
621                 undecodedChunk.readerIndex(firstpos);
622             }
623         } catch (ErrorDataDecoderException e) {
624             // error while decoding
625             undecodedChunk.readerIndex(firstpos);
626             throw e;
627         } catch (IOException e) {
628             // error while decoding
629             undecodedChunk.readerIndex(firstpos);
630             throw new ErrorDataDecoderException(e);
631         }
632     }
633 
634     private void setFinalBuffer(ByteBuf buffer) throws IOException {
635         currentAttribute.addContent(buffer, true);
636         String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset);
637         currentAttribute.setValue(value);
638         addHttpData(currentAttribute);
639         currentAttribute = null;
640     }
641 
642     /**
643      * Decode component
644      *
645      * @return the decoded component
646      */
647     private static String decodeAttribute(String s, Charset charset) {
648         try {
649             return QueryStringDecoder.decodeComponent(s, charset);
650         } catch (IllegalArgumentException e) {
651             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
652         }
653     }
654 
655     /**
656      * Skip control Characters
657      */
658     void skipControlCharacters() {
659         SeekAheadOptimize sao;
660         try {
661             sao = new SeekAheadOptimize(undecodedChunk);
662         } catch (SeekAheadNoBackArrayException ignored) {
663             try {
664                 skipControlCharactersStandard();
665             } catch (IndexOutOfBoundsException e) {
666                 throw new NotEnoughDataDecoderException(e);
667             }
668             return;
669         }
670 
671         while (sao.pos < sao.limit) {
672             char c = (char) (sao.bytes[sao.pos++] & 0xFF);
673             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
674                 sao.setReadPosition(1);
675                 return;
676             }
677         }
678         throw new NotEnoughDataDecoderException("Access out of bounds");
679     }
680 
681     void skipControlCharactersStandard() {
682         for (;;) {
683             char c = (char) undecodedChunk.readUnsignedByte();
684             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
685                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
686                 break;
687             }
688         }
689     }
690 
691     /**
692      * Destroy the {@link HttpPostStandardRequestDecoder} and release all it resources. After this method
693      * was called it is not possible to operate on it anymore.
694      */
695     @Override
696     public void destroy() {
697         checkDestroyed();
698         cleanFiles();
699         destroyed = true;
700 
701         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
702             undecodedChunk.release();
703             undecodedChunk = null;
704         }
705 
706         // release all data which was not yet pulled
707         for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
708             bodyListHttpData.get(i).release();
709         }
710     }
711 
712     /**
713      * Clean all HttpDatas (on Disk) for the current request.
714      */
715     @Override
716     public void cleanFiles() {
717         checkDestroyed();
718 
719         factory.cleanRequestHttpData(request);
720     }
721 
722     /**
723      * Remove the given FileUpload from the list of FileUploads to clean
724      */
725     @Override
726     public void removeHttpDataFromClean(InterfaceHttpData data) {
727         checkDestroyed();
728 
729         factory.removeHttpDataFromClean(request, data);
730     }
731 }