Class Gem::Server
In: lib/rubygems/server.rb
Parent: Object

Gem::Server and allows users to serve gems for consumption by `gem —remote-install`.

gem_server starts an HTTP server on the given port and serves the following:

Usage

  gem_server = Gem::Server.new Gem.dir, 8089, false
  gem_server.run

Methods

Marshal   add_date   latest_specs   listen   new   quick   rdoc   root   run   run   show_rdoc_for_pattern   specs   yaml  

Included Modules

ERB::Util Gem::UserInteraction

Constants

SEARCH = <<-SEARCH <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc"> <div id="search" style="float:right"> <label for="q">Filter/Search</label> <input id="q" type="text" style="width:10em" name="q"> <button type="submit" style="display:none"></button> </div> </form> SEARCH
DOC_TEMPLATE = <<-'DOC_TEMPLATE' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>RubyGems Documentation Index</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>RubyGems Documentation Index</h1> </div> <!-- banner header --> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <h1>Summary</h1> <p>There are <%=values["gem_count"]%> gems installed:</p> <p> <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>. <h1>Gems</h1> <dl> <% values["specs"].each do |spec| %> <dt> <% if spec["first_name_entry"] then %> <a name="<%=spec["name"]%>"></a> <% end %> <b><%=spec["name"]%> <%=spec["version"]%></b> <% if spec["rdoc_installed"] then %> <a href="<%=spec["doc_path"]%>">[rdoc]</a> <% else %> <span title="rdoc not installed">[rdoc]</span> <% end %> <% if spec["homepage"] then %> <a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a> <% else %> <span title="no homepage available">[www]</span> <% end %> <% if spec["has_deps"] then %> - depends on <%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>. <% end %> </dt> <dd> <%=spec["summary"]%> <% if spec["executables"] then %> <br/> <% if spec["only_one_executable"] then %> Executable is <% else %> Executables are <%end%> <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>. <%end%> <br/> <br/> </dd> <% end %> </dl> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> DOC_TEMPLATE
RDOC_CSS = <<-RDOC_CSS body { font-family: Verdana,Arial,Helvetica,sans-serif; font-size: 90%; margin: 0; margin-left: 40px; padding: 0; background: white; } h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } h1 { font-size: 150%; } h2,h3,h4 { margin-top: 1em; } a { background: #eef; color: #039; text-decoration: none; } a:hover { background: #039; color: #eef; } /* Override the base stylesheets Anchor inside a table cell */ td > a { background: transparent; color: #039; text-decoration: none; } /* and inside a section title */ .section-title > a { background: transparent; color: #eee; text-decoration: none; } /* === Structural elements =================================== */ div#index { margin: 0; margin-left: -40px; padding: 0; font-size: 90%; } div#index a { margin-left: 0.7em; } div#index .section-bar { margin-left: 0px; padding-left: 0.7em; background: #ccc; font-size: small; } div#classHeader, div#fileHeader { width: auto; color: white; padding: 0.5em 1.5em 0.5em 1.5em; margin: 0; margin-left: -40px; border-bottom: 3px solid #006; } div#classHeader a, div#fileHeader a { background: inherit; color: white; } div#classHeader td, div#fileHeader td { background: inherit; color: white; } div#fileHeader { background: #057; } div#classHeader { background: #048; } .class-name-in-header { font-size: 180%; font-weight: bold; } div#bodyContent { padding: 0 1.5em 0 1.5em; } div#description { padding: 0.5em 1.5em; background: #efefef; border: 1px dotted #999; } div#description h1,h2,h3,h4,h5,h6 { color: #125;; background: transparent; } div#validator-badges { text-align: center; } div#validator-badges img { border: 0; } div#copyright { color: #333; background: #efefef; font: 0.75em sans-serif; margin-top: 5em; margin-bottom: 0; padding: 0.5em 2em; } /* === Classes =================================== */ table.header-table { color: white; font-size: small; } .type-note { font-size: small; color: #DEDEDE; } .xxsection-bar { background: #eee; color: #333; padding: 3px; } .section-bar { color: #333; border-bottom: 1px solid #999; margin-left: -20px; } .section-title { background: #79a; color: #eee; padding: 3px; margin-top: 2em; margin-left: -30px; border: 1px solid #999; } .top-aligned-row { vertical-align: top } .bottom-aligned-row { vertical-align: bottom } /* --- Context section classes ----------------------- */ .context-row { } .context-item-name { font-family: monospace; font-weight: bold; color: black; } .context-item-value { font-size: small; color: #448; } .context-item-desc { color: #333; padding-left: 2em; } /* --- Method classes -------------------------- */ .method-detail { background: #efefef; padding: 0; margin-top: 0.5em; margin-bottom: 1em; border: 1px dotted #ccc; } .method-heading { color: black; background: #ccc; border-bottom: 1px solid #666; padding: 0.2em 0.5em 0 0.5em; } .method-signature { color: black; background: inherit; } .method-name { font-weight: bold; } .method-args { font-style: italic; } .method-description { padding: 0 0.5em 0 0.5em; } /* --- Source code sections -------------------- */ a.source-toggle { font-size: 90%; } div.method-source-code { background: #262626; color: #ffdead; margin: 1em; padding: 0.5em; border: 1px dashed #999; overflow: hidden; } div.method-source-code pre { color: #ffdead; overflow: hidden; } /* --- Ruby keyword styles --------------------- */ .standalone-code { background: #221111; color: #ffdead; overflow: hidden; } .ruby-constant { color: #7fffd4; background: transparent; } .ruby-keyword { color: #00ffff; background: transparent; } .ruby-ivar { color: #eedd82; background: transparent; } .ruby-operator { color: #00ffee; background: transparent; } .ruby-identifier { color: #ffdead; background: transparent; } .ruby-node { color: #ffa07a; background: transparent; } .ruby-comment { color: #b22222; font-weight: bold; background: transparent; } .ruby-regexp { color: #ffa07a; background: transparent; } .ruby-value { color: #7fffd4; background: transparent; } RDOC_CSS   CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
RDOC_NO_DOCUMENTATION = <<-'NO_DOC' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Found documentation</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>No documentation found</h1> </div> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <p>No gems matched <%= h query.inspect %></p> <p> Back to <a href="/">complete gem index</a> </p> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> NO_DOC
RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Found documentation</title> <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> </head> <body> <div id="fileHeader"> <%= SEARCH %> <h1>Found documentation</h1> </div> <!-- banner header --> <div id="bodyContent"> <div id="contextContent"> <div id="description"> <h1>Summary</h1> <p><%=doc_items.length%> documentation topics found.</p> <h1>Topics</h1> <dl> <% doc_items.each do |doc_item| %> <dt> <b><%=doc_item[:name]%></b> <a href="<%=doc_item[:url]%>">[rdoc]</a> </dt> <dd> <%=doc_item[:summary]%> <br/> <br/> </dd> <% end %> </dl> <p> Back to <a href="/">complete gem index</a> </p> </div> </div> </div> <div id="validator-badges"> <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> </div> </body> </html> RDOC_SEARCH

Attributes

spec_dirs  [R] 

Public Class methods

Only the first directory in gem_dirs is used for serving gems

[Source]

     # File lib/rubygems/server.rb, line 443
443:   def initialize(gem_dirs, port, daemon, addresses = nil)
444:     Socket.do_not_reverse_lookup = true
445: 
446:     @gem_dirs = Array gem_dirs
447:     @port = port
448:     @daemon = daemon
449:     @addresses = addresses
450:     logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
451:     @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
452: 
453:     @spec_dirs = @gem_dirs.map do |gem_dir|
454:       spec_dir = File.join gem_dir, 'specifications'
455: 
456:       unless File.directory? spec_dir then
457:         raise ArgumentError, "#{gem_dir} does not appear to be a gem repository"
458:       end
459: 
460:       spec_dir
461:     end
462: 
463:     @source_index = Gem::SourceIndex.from_gems_in(*@spec_dirs)
464:   end

[Source]

     # File lib/rubygems/server.rb, line 435
435:   def self.run(options)
436:     new(options[:gemdir], options[:port], options[:daemon],
437:         options[:addresses]).run
438:   end

Public Instance methods

[Source]

     # File lib/rubygems/server.rb, line 466
466:   def Marshal(req, res)
467:     @source_index.refresh!
468: 
469:     add_date res
470: 
471:     index = Marshal.dump @source_index
472: 
473:     if req.request_method == 'HEAD' then
474:       res['content-length'] = index.length
475:       return
476:     end
477: 
478:     if req.path =~ /Z$/ then
479:       res['content-type'] = 'application/x-deflate'
480:       index = Gem.deflate index
481:     else
482:       res['content-type'] = 'application/octet-stream'
483:     end
484: 
485:     res.body << index
486:   end

[Source]

     # File lib/rubygems/server.rb, line 488
488:   def add_date res
489:     res['date'] = @spec_dirs.map do |spec_dir|
490:       File.stat(spec_dir).mtime
491:     end.max
492:   end

[Source]

     # File lib/rubygems/server.rb, line 494
494:   def latest_specs(req, res)
495:     @source_index.refresh!
496: 
497:     res['content-type'] = 'application/x-gzip'
498: 
499:     add_date res
500: 
501:     specs = @source_index.latest_specs.sort.map do |spec|
502:       platform = spec.original_platform
503:       platform = Gem::Platform::RUBY if platform.nil?
504:       [spec.name, spec.version, platform]
505:     end
506: 
507:     specs = Marshal.dump specs
508: 
509:     if req.path =~ /\.gz$/ then
510:       specs = Gem.gzip specs
511:       res['content-type'] = 'application/x-gzip'
512:     else
513:       res['content-type'] = 'application/octet-stream'
514:     end
515: 
516:     if req.request_method == 'HEAD' then
517:       res['content-length'] = specs.length
518:     else
519:       res.body << specs
520:     end
521:   end

Creates server sockets based on the addresses option. If no addresses were given a server socket for all interfaces is created.

[Source]

     # File lib/rubygems/server.rb, line 527
527:   def listen addresses = @addresses
528:     addresses = [nil] unless addresses
529: 
530:     listeners = 0
531: 
532:     addresses.each do |address|
533:       begin
534:         @server.listen address, @port
535:         @server.listeners[listeners..-1].each do |listener|
536:           host, port = listener.addr.values_at 2, 1
537:           host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
538:           say "Server started at http://#{host}:#{port}"
539:         end
540: 
541:         listeners = @server.listeners.length
542:       rescue SystemCallError
543:         next
544:       end
545:     end
546: 
547:     if @server.listeners.empty? then
548:       say "Unable to start a server."
549:       say "Check for running servers or your --bind and --port arguments"
550:       terminate_interaction 1
551:     end
552:   end

[Source]

     # File lib/rubygems/server.rb, line 554
554:   def quick(req, res)
555:     @source_index.refresh!
556: 
557:     res['content-type'] = 'text/plain'
558:     add_date res
559: 
560:     case req.request_uri.path
561:     when '/quick/index' then
562:       res.body << @source_index.map { |name,| name }.sort.join("\n")
563:     when '/quick/index.rz' then
564:       index = @source_index.map { |name,| name }.sort.join("\n")
565:       res['content-type'] = 'application/x-deflate'
566:       res.body << Gem.deflate(index)
567:     when '/quick/latest_index' then
568:       index = @source_index.latest_specs.map { |spec| spec.full_name }
569:       res.body << index.sort.join("\n")
570:     when '/quick/latest_index.rz' then
571:       index = @source_index.latest_specs.map { |spec| spec.full_name }
572:       res['content-type'] = 'application/x-deflate'
573:       res.body << Gem.deflate(index.sort.join("\n"))
574:     when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
575:       dep = Gem::Dependency.new $2, $3
576:       specs = @source_index.search dep
577:       marshal_format = $1
578: 
579:       selector = [$2, $3, $4].map { |s| s.inspect }.join ' '
580: 
581:       platform = if $4 then
582:                    Gem::Platform.new $4.sub(/^-/, '')
583:                  else
584:                    Gem::Platform::RUBY
585:                  end
586: 
587:       specs = specs.select { |s| s.platform == platform }
588: 
589:       if specs.empty? then
590:         res.status = 404
591:         res.body = "No gems found matching #{selector}"
592:       elsif specs.length > 1 then
593:         res.status = 500
594:         res.body = "Multiple gems found matching #{selector}"
595:       elsif marshal_format then
596:         res['content-type'] = 'application/x-deflate'
597:         res.body << Gem.deflate(Marshal.dump(specs.first))
598:       else # deprecated YAML format
599:         res['content-type'] = 'application/x-deflate'
600:         res.body << Gem.deflate(specs.first.to_yaml)
601:       end
602:     else
603:       raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
604:     end
605:   end

Can be used for quick navigation to the rdoc documentation. You can then define a search shortcut for your browser. E.g. in Firefox connect ‘shortcut:rdoc’ to localhost:8808/rdoc?q=%s template. Then you can directly open the ActionPack documentation by typing ‘rdoc actionp’. If there are multiple hits for the search term, they are presented as a list with links.

Search algorithm aims for an intuitive search:

  1. first try to find the gems and documentation folders which name starts with the search term
  2. search for entries, that contain the search term
  3. show all the gems

If there is only one search hit, user is immediately redirected to the documentation for the particular gem, otherwise a list with results is shown.

Additional trick - install documentation for ruby core

Note: please adjust paths accordingly use for example ‘locate yaml.rb’ and ‘gem environment’ to identify directories, that are specific for your local installation

  1. install ruby sources
      cd /usr/src
      sudo apt-get source ruby
    
  2. generate documentation
      rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc    #        /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
    

By typing ‘rdoc core’ you can now access the core documentation

[Source]

     # File lib/rubygems/server.rb, line 721
721:   def rdoc(req, res)
722:     query = req.query['q']
723:     show_rdoc_for_pattern("#{query}*", res) && return
724:     show_rdoc_for_pattern("*#{query}*", res) && return
725: 
726:     template = ERB.new RDOC_NO_DOCUMENTATION
727: 
728:     res['content-type'] = 'text/html'
729:     res.body = template.result binding
730:   end

[Source]

     # File lib/rubygems/server.rb, line 607
607:   def root(req, res)
608:     @source_index.refresh!
609:     add_date res
610: 
611:     raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
612:       req.path == '/'
613: 
614:     specs = []
615:     total_file_count = 0
616: 
617:     @source_index.each do |path, spec|
618:       total_file_count += spec.files.size
619:       deps = spec.dependencies.map do |dep|
620:         { "name"    => dep.name,
621:           "type"    => dep.type,
622:           "version" => dep.requirement.to_s, }
623:       end
624: 
625:       deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
626:       deps.last["is_last"] = true unless deps.empty?
627: 
628:       # executables
629:       executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
630:       executables = nil if executables.empty?
631:       executables.last["is_last"] = true if executables
632: 
633:       specs << {
634:         "authors"             => spec.authors.sort.join(", "),
635:         "date"                => spec.date.to_s,
636:         "dependencies"        => deps,
637:         "doc_path"            => "/doc_root/#{spec.full_name}/rdoc/index.html",
638:         "executables"         => executables,
639:         "only_one_executable" => (executables && executables.size == 1),
640:         "full_name"           => spec.full_name,
641:         "has_deps"            => !deps.empty?,
642:         "homepage"            => spec.homepage,
643:         "name"                => spec.name,
644:         "rdoc_installed"      => Gem::DocManager.new(spec).rdoc_installed?,
645:         "summary"             => spec.summary,
646:         "version"             => spec.version.to_s,
647:       }
648:     end
649: 
650:     specs << {
651:       "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
652:       "dependencies" => [],
653:       "doc_path" => "/doc_root/rubygems-#{Gem::VERSION}/rdoc/index.html",
654:       "executables" => [{"executable" => 'gem', "is_last" => true}],
655:       "only_one_executable" => true,
656:       "full_name" => "rubygems-#{Gem::VERSION}",
657:       "has_deps" => false,
658:       "homepage" => "http://docs.rubygems.org/",
659:       "name" => 'rubygems',
660:       "rdoc_installed" => true,
661:       "summary" => "RubyGems itself",
662:       "version" => Gem::VERSION,
663:     }
664: 
665:     specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
666:     specs.last["is_last"] = true
667: 
668:     # tag all specs with first_name_entry
669:     last_spec = nil
670:     specs.each do |spec|
671:       is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
672:       spec["first_name_entry"] = is_first
673:       last_spec = spec
674:     end
675: 
676:     # create page from template
677:     template = ERB.new(DOC_TEMPLATE)
678:     res['content-type'] = 'text/html'
679: 
680:     values = { "gem_count" => specs.size.to_s, "specs" => specs,
681:                "total_file_count" => total_file_count.to_s }
682: 
683:     result = template.result binding
684:     res.body = result
685:   end

[Source]

     # File lib/rubygems/server.rb, line 771
771:   def run
772:     listen
773: 
774:     WEBrick::Daemon.start if @daemon
775: 
776:     @server.mount_proc "/yaml", method(:yaml)
777:     @server.mount_proc "/yaml.Z", method(:yaml)
778: 
779:     @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal)
780:     @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal)
781: 
782:     @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
783:     @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
784: 
785:     @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
786:                        method(:latest_specs)
787:     @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
788:                        method(:latest_specs)
789: 
790:     @server.mount_proc "/quick/", method(:quick)
791: 
792:     @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
793:       res['content-type'] = 'text/css'
794:       add_date res
795:       res.body << RDOC_CSS
796:     end
797: 
798:     @server.mount_proc "/", method(:root)
799: 
800:     @server.mount_proc "/rdoc", method(:rdoc)
801: 
802:     paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
803:     paths.each do |mount_point, mount_dir|
804:       @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
805:                     File.join(@gem_dirs.first, mount_dir), true)
806:     end
807: 
808:     trap("INT") { @server.shutdown; exit! }
809:     trap("TERM") { @server.shutdown; exit! }
810: 
811:     @server.start
812:   end

Returns true and prepares http response, if rdoc for the requested gem name pattern was found.

The search is based on the file system content, not on the gems metadata. This allows additional documentation folders like ‘core’ for the ruby core documentation - just put it underneath the main doc folder.

[Source]

     # File lib/rubygems/server.rb, line 740
740:   def show_rdoc_for_pattern(pattern, res)
741:     found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path|
742:       File.exist? File.join(path, 'rdoc/index.html')
743:     }
744:     case found_gems.length
745:     when 0
746:       return false
747:     when 1
748:       new_path = File.basename(found_gems[0])
749:       res.status = 302
750:       res['Location'] = "/doc_root/#{new_path}/rdoc/index.html"
751:       return true
752:     else
753:       doc_items = []
754:       found_gems.each do |file_name|
755:         base_name = File.basename(file_name)
756:         doc_items << {
757:           :name => base_name,
758:           :url => "/doc_root/#{base_name}/rdoc/index.html",
759:           :summary => ''
760:         }
761:       end
762: 
763:       template = ERB.new(RDOC_SEARCH_TEMPLATE)
764:       res['content-type'] = 'text/html'
765:       result = template.result binding
766:       res.body = result
767:       return true
768:     end
769:   end

[Source]

     # File lib/rubygems/server.rb, line 814
814:   def specs(req, res)
815:     @source_index.refresh!
816: 
817:     add_date res
818: 
819:     specs = @source_index.sort.map do |_, spec|
820:       platform = spec.original_platform
821:       platform = Gem::Platform::RUBY if platform.nil?
822:       [spec.name, spec.version, platform]
823:     end
824: 
825:     specs = Marshal.dump specs
826: 
827:     if req.path =~ /\.gz$/ then
828:       specs = Gem.gzip specs
829:       res['content-type'] = 'application/x-gzip'
830:     else
831:       res['content-type'] = 'application/octet-stream'
832:     end
833: 
834:     if req.request_method == 'HEAD' then
835:       res['content-length'] = specs.length
836:     else
837:       res.body << specs
838:     end
839:   end

[Source]

     # File lib/rubygems/server.rb, line 841
841:   def yaml(req, res)
842:     @source_index.refresh!
843: 
844:     add_date res
845: 
846:     index = @source_index.to_yaml
847: 
848:     if req.path =~ /Z$/ then
849:       res['content-type'] = 'application/x-deflate'
850:       index = Gem.deflate index
851:     else
852:       res['content-type'] = 'text/plain'
853:     end
854: 
855:     if req.request_method == 'HEAD' then
856:       res['content-length'] = index.length
857:       return
858:     end
859: 
860:     res.body << index
861:   end

[Validate]